From 2d45a13249943cafa73460b35c3c366e5742eb6f Mon Sep 17 00:00:00 2001 From: jwilson Date: Thu, 31 Jan 2013 17:36:19 -0500 Subject: [PATCH] Mechanically change OkHttp to Square's Java style. --- .../java/com/squareup/okhttp/Address.java | 137 +- .../java/com/squareup/okhttp/Connection.java | 492 +- .../com/squareup/okhttp/ConnectionPool.java | 46 +- .../com/squareup/okhttp/OkHttpClient.java | 239 +- .../com/squareup/okhttp/OkResponseCache.java | 20 +- .../com/squareup/okhttp/ResponseSource.java | 34 +- .../com/squareup/okhttp/TunnelRequest.java | 85 +- .../com/squareup/okhttp/internal/Base64.java | 262 +- .../okhttp/internal/DiskLruCache.java | 1331 +++-- .../com/squareup/okhttp/internal/Dns.java | 12 +- .../squareup/okhttp/internal/Platform.java | 545 +- .../okhttp/internal/StrictLineReader.java | 337 +- .../com/squareup/okhttp/internal/Util.java | 581 ++- .../http/AbstractHttpInputStream.java | 118 +- .../http/AbstractHttpOutputStream.java | 16 +- .../okhttp/internal/http/HeaderParser.java | 168 +- .../internal/http/HttpAuthenticator.java | 255 +- .../okhttp/internal/http/HttpDate.java | 94 +- .../okhttp/internal/http/HttpEngine.java | 1062 ++-- .../internal/http/HttpResponseCache.java | 993 ++-- .../okhttp/internal/http/HttpTransport.java | 838 ++- .../internal/http/HttpURLConnectionImpl.java | 784 ++- .../internal/http/HttpsURLConnectionImpl.java | 781 ++- .../okhttp/internal/http/RawHeaders.java | 715 ++- .../okhttp/internal/http/RequestHeaders.java | 490 +- .../okhttp/internal/http/ResponseHeaders.java | 764 ++- .../internal/http/RetryableOutputStream.java | 71 +- .../okhttp/internal/http/RouteSelector.java | 355 +- .../okhttp/internal/http/SpdyTransport.java | 99 +- .../okhttp/internal/http/Transport.java | 74 +- .../http/UnknownLengthHttpInputStream.java | 69 +- .../internal/spdy/IncomingStreamHandler.java | 28 +- .../squareup/okhttp/internal/spdy/Ping.java | 78 +- .../okhttp/internal/spdy/Settings.java | 268 +- .../okhttp/internal/spdy/SpdyConnection.java | 987 ++-- .../okhttp/internal/spdy/SpdyReader.java | 527 +- .../okhttp/internal/spdy/SpdyStream.java | 1266 +++-- .../okhttp/internal/spdy/SpdyWriter.java | 274 +- .../squareup/okhttp/ConnectionPoolTest.java | 16 +- .../okhttp/internal/DiskLruCacheTest.java | 1486 +++--- .../internal/RecordingAuthenticator.java | 51 +- .../internal/RecordingHostnameVerifier.java | 10 +- .../okhttp/internal/SslContextBuilder.java | 156 +- .../okhttp/internal/StrictLineReaderTest.java | 98 +- .../internal/http/ExternalSpdyExample.java | 33 +- .../internal/http/HttpResponseCacheTest.java | 3230 ++++++------ .../okhttp/internal/http/RawHeadersTest.java | 85 +- .../internal/http/RouteSelectorTest.java | 620 ++- .../internal/http/URLConnectionTest.java | 4525 ++++++++--------- .../okhttp/internal/http/URLEncodingTest.java | 208 +- .../mockspdyserver/MockSpdyServer.java | 388 +- .../internal/spdy/HttpOverSpdyTest.java | 432 +- .../okhttp/internal/spdy/MockSpdyPeer.java | 410 +- .../okhttp/internal/spdy/SettingsTest.java | 195 +- .../internal/spdy/SpdyConnectionTest.java | 1933 ++++--- .../okhttp/internal/spdy/SpdyServer.java | 239 +- 56 files changed, 14508 insertions(+), 14902 deletions(-) diff --git a/src/main/java/com/squareup/okhttp/Address.java b/src/main/java/com/squareup/okhttp/Address.java index 430eff5ed..cd41ac99c 100644 --- a/src/main/java/com/squareup/okhttp/Address.java +++ b/src/main/java/com/squareup/okhttp/Address.java @@ -15,12 +15,13 @@ */ package com.squareup.okhttp; -import static com.squareup.okhttp.internal.Util.equal; import java.net.Proxy; import java.net.UnknownHostException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; +import static com.squareup.okhttp.internal.Util.equal; + /** * A specification for a connection to an origin server. For simple connections, * this is the server's hostname and port. If an explicit proxy is requested (or @@ -32,81 +33,79 @@ import javax.net.ssl.SSLSocketFactory; * {@link Connection}. */ public final class Address { - final Proxy proxy; - final String uriHost; - final int uriPort; - final SSLSocketFactory sslSocketFactory; - final HostnameVerifier hostnameVerifier; + final Proxy proxy; + final String uriHost; + final int uriPort; + final SSLSocketFactory sslSocketFactory; + final HostnameVerifier hostnameVerifier; - public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory, - HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException { - if (uriHost == null) throw new NullPointerException("uriHost == null"); - if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort); - this.proxy = proxy; - this.uriHost = uriHost; - this.uriPort = uriPort; - this.sslSocketFactory = sslSocketFactory; - this.hostnameVerifier = hostnameVerifier; - } + public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory, + HostnameVerifier hostnameVerifier, Proxy proxy) throws UnknownHostException { + if (uriHost == null) throw new NullPointerException("uriHost == null"); + if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort); + this.proxy = proxy; + this.uriHost = uriHost; + this.uriPort = uriPort; + this.sslSocketFactory = sslSocketFactory; + this.hostnameVerifier = hostnameVerifier; + } - /** - * Returns the hostname of the origin server. - */ - public String getUriHost() { - return uriHost; - } + /** Returns the hostname of the origin server. */ + public String getUriHost() { + return uriHost; + } - /** - * Returns the port of the origin server; typically 80 or 443. Unlike - * may {@code getPort()} accessors, this method never returns -1. - */ - public int getUriPort() { - return uriPort; - } + /** + * Returns the port of the origin server; typically 80 or 443. Unlike + * may {@code getPort()} accessors, this method never returns -1. + */ + public int getUriPort() { + return uriPort; + } - /** - * Returns the SSL socket factory, or null if this is not an HTTPS - * address. - */ - public SSLSocketFactory getSslSocketFactory() { - return sslSocketFactory; - } + /** + * Returns the SSL socket factory, or null if this is not an HTTPS + * address. + */ + public SSLSocketFactory getSslSocketFactory() { + return sslSocketFactory; + } - /** - * Returns the hostname verifier, or null if this is not an HTTPS - * address. - */ - public HostnameVerifier getHostnameVerifier() { - return hostnameVerifier; - } + /** + * Returns the hostname verifier, or null if this is not an HTTPS + * address. + */ + public HostnameVerifier getHostnameVerifier() { + return hostnameVerifier; + } - /** - * Returns this address's explicitly-specified HTTP proxy, or null to - * delegate to the HTTP client's proxy selector. - */ - public Proxy getProxy() { - return proxy; - } + /** + * Returns this address's explicitly-specified HTTP proxy, or null to + * delegate to the HTTP client's proxy selector. + */ + public Proxy getProxy() { + return proxy; + } - @Override public boolean equals(Object other) { - if (other instanceof Address) { - Address that = (Address) other; - return equal(this.proxy, that.proxy) - && this.uriHost.equals(that.uriHost) - && this.uriPort == that.uriPort - && equal(this.sslSocketFactory, that.sslSocketFactory) - && equal(this.hostnameVerifier, that.hostnameVerifier); - } - return false; + @Override public boolean equals(Object other) { + if (other instanceof Address) { + Address that = (Address) other; + return equal(this.proxy, that.proxy) + && this.uriHost.equals(that.uriHost) + && this.uriPort == that.uriPort + && equal(this.sslSocketFactory, that.sslSocketFactory) + && equal(this.hostnameVerifier, that.hostnameVerifier); } + return false; + } - @Override public int hashCode() { - int result = 17; - result = 31 * result + uriHost.hashCode(); - result = 31 * result + uriPort; - result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); - result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0); - result = 31 * result + (proxy != null ? proxy.hashCode() : 0); - return result; - } + @Override public int hashCode() { + int result = 17; + result = 31 * result + uriHost.hashCode(); + result = 31 * result + uriPort; + result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); + result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0); + result = 31 * result + (proxy != null ? proxy.hashCode() : 0); + return result; + } } diff --git a/src/main/java/com/squareup/okhttp/Connection.java b/src/main/java/com/squareup/okhttp/Connection.java index d6a7e53e3..c46f7edc6 100644 --- a/src/main/java/com/squareup/okhttp/Connection.java +++ b/src/main/java/com/squareup/okhttp/Connection.java @@ -28,8 +28,6 @@ import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import static java.net.HttpURLConnection.HTTP_OK; -import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; import java.net.InetSocketAddress; import java.net.Proxy; import java.net.Socket; @@ -37,6 +35,9 @@ import java.net.URL; import java.util.Arrays; import javax.net.ssl.SSLSocket; +import static java.net.HttpURLConnection.HTTP_OK; +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; + /** * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection, * which may be used for multiple HTTP request/response exchanges. Connections @@ -53,10 +54,10 @@ import javax.net.ssl.SSLSocket; * There are tradeoffs when selecting which options to include when negotiating * a secure connection to a remote host. Newer TLS options are quite useful: * * Unfortunately, older HTTPS servers refuse to connect when such options are * presented. Rather than avoiding these options entirely, this class allows a @@ -64,268 +65,251 @@ import javax.net.ssl.SSLSocket; * should the attempt fail. */ public final class Connection implements Closeable { - private static final byte[] NPN_PROTOCOLS = new byte[] { - 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_11 = new byte[] { - 'h', 't', 't', 'p', '/', '1', '.', '1', - }; + private static final byte[] NPN_PROTOCOLS = + new byte[] { 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_11 = new byte[] { 'h', 't', 't', 'p', '/', '1', '.', '1', }; - private final Address address; - private final Proxy proxy; - private final InetSocketAddress inetSocketAddress; - private final boolean modernTls; + private final Address address; + private final Proxy proxy; + private final InetSocketAddress inetSocketAddress; + private final boolean modernTls; - private Socket socket; - private InputStream in; - private OutputStream out; - private boolean connected = false; - private SpdyConnection spdyConnection; - private int httpMinorVersion = 1; // Assume HTTP/1.1 - private long idleStartTimeNs; + private Socket socket; + private InputStream in; + private OutputStream out; + private boolean connected = false; + private SpdyConnection spdyConnection; + private int httpMinorVersion = 1; // Assume HTTP/1.1 + private long idleStartTimeNs; - public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, - boolean modernTls) { - if (address == null) throw new NullPointerException("address == null"); - if (proxy == null) throw new NullPointerException("proxy == null"); - if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null"); - this.address = address; - this.proxy = proxy; - this.inetSocketAddress = inetSocketAddress; - this.modernTls = modernTls; + public Connection(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, + boolean modernTls) { + if (address == null) throw new NullPointerException("address == null"); + if (proxy == null) throw new NullPointerException("proxy == null"); + if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null"); + this.address = address; + this.proxy = proxy; + this.inetSocketAddress = inetSocketAddress; + this.modernTls = modernTls; + } + + public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) + throws IOException { + if (connected) { + throw new IllegalStateException("already connected"); + } + connected = true; + socket = (proxy.type() != Proxy.Type.HTTP) ? new Socket(proxy) : new Socket(); + socket.connect(inetSocketAddress, connectTimeout); + socket.setSoTimeout(readTimeout); + in = socket.getInputStream(); + out = socket.getOutputStream(); + + if (address.sslSocketFactory != null) { + upgradeToTls(tunnelRequest); } - public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) - throws IOException { - if (connected) { - throw new IllegalStateException("already connected"); - } - connected = true; - socket = (proxy.type() != Proxy.Type.HTTP) - ? new Socket(proxy) - : new Socket(); - socket.connect(inetSocketAddress, connectTimeout); - socket.setSoTimeout(readTimeout); - in = socket.getInputStream(); - out = socket.getOutputStream(); - - if (address.sslSocketFactory != null) { - upgradeToTls(tunnelRequest); - } - - // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes. - if (!isSpdy()) { - int bufferSize = 128; - in = new BufferedInputStream(in, bufferSize); - } - } - - /** - * Create an {@code SSLSocket} and perform the TLS handshake and certificate - * validation. - */ - private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException { - Platform platform = Platform.get(); - - // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. - if (requiresTunnel()) { - makeTunnel(tunnelRequest); - } - - // Create the wrapper over connected socket. - socket = address.sslSocketFactory.createSocket( - socket, address.uriHost, address.uriPort, true /* autoClose */); - SSLSocket sslSocket = (SSLSocket) socket; - if (modernTls) { - platform.enableTlsExtensions(sslSocket, address.uriHost); - } else { - platform.supportTlsIntolerantServer(sslSocket); - } - - if (modernTls) { - platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); - } - - // Force handshake. This can throw! - sslSocket.startHandshake(); - - // Verify that the socket's certificates are acceptable for the target host. - if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) { - throw new IOException("Hostname '" + address.uriHost + "' was not verified"); - } - - out = sslSocket.getOutputStream(); - in = sslSocket.getInputStream(); - - byte[] selectedProtocol; - if (modernTls - && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { - 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)) { - throw new IOException("Unexpected NPN transport " - + new String(selectedProtocol, "ISO-8859-1")); - } - } - } - - /** - * Returns true if {@link #connect} has been attempted on this connection. - */ - public boolean isConnected() { - return connected; - } - - @Override public void close() throws IOException { - socket.close(); - } - - /** - * Returns the proxy that this connection is using. - * - * Warning: This may be different than the proxy returned - * by {@link #getAddress}! That is the proxy that the user asked to be - * connected to; this returns the proxy that they were actually connected - * to. The two may disagree when a proxy selector selects a different proxy - * for a connection. - */ - public Proxy getProxy() { - return proxy; - } - - public Address getAddress() { - return address; - } - - public InetSocketAddress getSocketAddress() { - return inetSocketAddress; - } - - public boolean isModernTls() { - return modernTls; - } - - /** - * Returns the socket that this connection uses, or null if the connection - * is not currently connected. - */ - public Socket getSocket() { - return socket; - } - - /** - * Returns true if this connection is alive. - */ - public boolean isAlive() { - return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); - } - - public void resetIdleStartTime() { - if (spdyConnection != null) { - throw new IllegalStateException("spdyConnection != null"); - } - this.idleStartTimeNs = System.nanoTime(); - } - - /** - * Returns true if this connection is idle. - */ - public boolean isIdle() { - return spdyConnection == null || spdyConnection.isIdle(); - } - - /** - * Returns true if this connection has been idle for longer than - * {@code keepAliveDurationNs}. - */ - public boolean isExpired(long keepAliveDurationNs) { - return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs; - } - - /** - * Returns the time in ns when this connection became idle. Undefined if - * this connection is not idle. - */ - public long getIdleStartTimeNs() { - return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs(); - } - - /** - * Returns the transport appropriate for this connection. - */ - public Object newTransport(HttpEngine httpEngine) throws IOException { - return (spdyConnection != null) - ? new SpdyTransport(httpEngine, spdyConnection) - : new HttpTransport(httpEngine, out, in); - } - - /** - * Returns true if this is a SPDY connection. Such connections can be used - * in multiple HTTP requests simultaneously. - */ - public boolean isSpdy() { - return spdyConnection != null; - } - - public SpdyConnection getSpdyConnection() { - return spdyConnection; + // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes. + if (!isSpdy()) { + int bufferSize = 128; + in = new BufferedInputStream(in, bufferSize); } + } /** - * Returns the minor HTTP version that should be used for future requests on - * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default - * value is 1 for new connections. - */ - public int getHttpMinorVersion() { - return httpMinorVersion; + * Create an {@code SSLSocket} and perform the TLS handshake and certificate + * validation. + */ + private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException { + Platform platform = Platform.get(); + + // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. + if (requiresTunnel()) { + makeTunnel(tunnelRequest); } - public void setHttpMinorVersion(int httpMinorVersion) { - this.httpMinorVersion = httpMinorVersion; + // Create the wrapper over connected socket. + socket = address.sslSocketFactory + .createSocket(socket, address.uriHost, address.uriPort, true /* autoClose */); + SSLSocket sslSocket = (SSLSocket) socket; + if (modernTls) { + platform.enableTlsExtensions(sslSocket, address.uriHost); + } else { + platform.supportTlsIntolerantServer(sslSocket); } - /** - * Returns true if the HTTP connection needs to tunnel one protocol over - * another, such as when using HTTPS through an HTTP proxy. When doing so, - * we must avoid buffering bytes intended for the higher-level protocol. - */ - public boolean requiresTunnel() { - return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP; + if (modernTls) { + platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); } - /** - * To make an HTTPS connection over an HTTP proxy, send an unencrypted - * CONNECT request to create the proxy connection. This may need to be - * retried if the proxy requires authorization. - */ - private void makeTunnel(TunnelRequest tunnelRequest) throws IOException { - RawHeaders requestHeaders = tunnelRequest.getRequestHeaders(); - while (true) { - out.write(requestHeaders.toBytes()); - RawHeaders responseHeaders = RawHeaders.fromBytes(in); + // Force handshake. This can throw! + sslSocket.startHandshake(); - switch (responseHeaders.getResponseCode()) { - case HTTP_OK: - return; - case HTTP_PROXY_AUTH: - requestHeaders = new RawHeaders(requestHeaders); - URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); - boolean credentialsFound = HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, - responseHeaders, requestHeaders, proxy, url); - if (credentialsFound) { - continue; - } else { - throw new IOException("Failed to authenticate with proxy"); - } - default: - throw new IOException("Unexpected response code for CONNECT: " - + responseHeaders.getResponseCode()); - } - } + // Verify that the socket's certificates are acceptable for the target host. + if (!address.hostnameVerifier.verify(address.uriHost, sslSocket.getSession())) { + throw new IOException("Hostname '" + address.uriHost + "' was not verified"); } + + out = sslSocket.getOutputStream(); + in = sslSocket.getInputStream(); + + byte[] selectedProtocol; + if (modernTls && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { + 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)) { + throw new IOException( + "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1")); + } + } + } + + /** Returns true if {@link #connect} has been attempted on this connection. */ + public boolean isConnected() { + return connected; + } + + @Override public void close() throws IOException { + socket.close(); + } + + /** + * Returns the proxy that this connection is using. + * + * Warning: This may be different than the proxy returned + * by {@link #getAddress}! That is the proxy that the user asked to be + * connected to; this returns the proxy that they were actually connected + * to. The two may disagree when a proxy selector selects a different proxy + * for a connection. + */ + public Proxy getProxy() { + return proxy; + } + + public Address getAddress() { + return address; + } + + public InetSocketAddress getSocketAddress() { + return inetSocketAddress; + } + + public boolean isModernTls() { + return modernTls; + } + + /** + * Returns the socket that this connection uses, or null if the connection + * is not currently connected. + */ + public Socket getSocket() { + return socket; + } + + /** Returns true if this connection is alive. */ + public boolean isAlive() { + return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); + } + + public void resetIdleStartTime() { + if (spdyConnection != null) { + throw new IllegalStateException("spdyConnection != null"); + } + this.idleStartTimeNs = System.nanoTime(); + } + + /** Returns true if this connection is idle. */ + public boolean isIdle() { + return spdyConnection == null || spdyConnection.isIdle(); + } + + /** + * Returns true if this connection has been idle for longer than + * {@code keepAliveDurationNs}. + */ + public boolean isExpired(long keepAliveDurationNs) { + return isIdle() && System.nanoTime() - getIdleStartTimeNs() > keepAliveDurationNs; + } + + /** + * Returns the time in ns when this connection became idle. Undefined if + * this connection is not idle. + */ + public long getIdleStartTimeNs() { + return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs(); + } + + /** Returns the transport appropriate for this connection. */ + public Object newTransport(HttpEngine httpEngine) throws IOException { + return (spdyConnection != null) ? new SpdyTransport(httpEngine, spdyConnection) + : new HttpTransport(httpEngine, out, in); + } + + /** + * Returns true if this is a SPDY connection. Such connections can be used + * in multiple HTTP requests simultaneously. + */ + public boolean isSpdy() { + return spdyConnection != null; + } + + public SpdyConnection getSpdyConnection() { + return spdyConnection; + } + + /** + * Returns the minor HTTP version that should be used for future requests on + * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default + * value is 1 for new connections. + */ + public int getHttpMinorVersion() { + return httpMinorVersion; + } + + public void setHttpMinorVersion(int httpMinorVersion) { + this.httpMinorVersion = httpMinorVersion; + } + + /** + * Returns true if the HTTP connection needs to tunnel one protocol over + * another, such as when using HTTPS through an HTTP proxy. When doing so, + * we must avoid buffering bytes intended for the higher-level protocol. + */ + public boolean requiresTunnel() { + return address.sslSocketFactory != null && proxy != null && proxy.type() == Proxy.Type.HTTP; + } + + /** + * To make an HTTPS connection over an HTTP proxy, send an unencrypted + * CONNECT request to create the proxy connection. This may need to be + * retried if the proxy requires authorization. + */ + private void makeTunnel(TunnelRequest tunnelRequest) throws IOException { + RawHeaders requestHeaders = tunnelRequest.getRequestHeaders(); + while (true) { + out.write(requestHeaders.toBytes()); + RawHeaders responseHeaders = RawHeaders.fromBytes(in); + + switch (responseHeaders.getResponseCode()) { + case HTTP_OK: + return; + case HTTP_PROXY_AUTH: + requestHeaders = new RawHeaders(requestHeaders); + URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); + boolean credentialsFound = + HttpAuthenticator.processAuthHeader(HTTP_PROXY_AUTH, responseHeaders, requestHeaders, + proxy, url); + if (credentialsFound) { + continue; + } else { + throw new IOException("Failed to authenticate with proxy"); + } + default: + throw new IOException( + "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode()); + } + } + } } diff --git a/src/main/java/com/squareup/okhttp/ConnectionPool.java b/src/main/java/com/squareup/okhttp/ConnectionPool.java index b32c9d2ac..be4eb1ddd 100644 --- a/src/main/java/com/squareup/okhttp/ConnectionPool.java +++ b/src/main/java/com/squareup/okhttp/ConnectionPool.java @@ -14,23 +14,27 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** - * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP requests that share - * the same {@link com.squareup.okhttp.Address} may share a {@link com.squareup.okhttp.Connection}. - * This class implements the policy of which connections to keep open for future use. + * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP + * requests that share the same {@link com.squareup.okhttp.Address} may share a + * {@link com.squareup.okhttp.Connection}. This class implements the policy of + * which connections to keep open for future use. * - *

The {@link #getDefault() system-wide default} uses system properties for tuning parameters: + *

The {@link #getDefault() system-wide default} uses system properties for + * tuning parameters: *

* - *

The default instance doesn't adjust its configuration as system properties are changed. - * This assumes that the applications that set these parameters do so before making HTTP - * connections, and that this class is initialized lazily. + *

The default instance doesn't adjust its configuration as system + * properties are changed. This assumes that the applications that set these + * parameters do so before making HTTP connections, and that this class is + * initialized lazily. */ public class ConnectionPool { private static final int MAX_CONNECTIONS_TO_CLEANUP = 2; @@ -78,8 +82,8 @@ public class ConnectionPool { } } - for (Iterator i = connections.descendingIterator(); i.hasNext() - && idleConnectionCount > maxIdleConnections; ) { + for (Iterator i = connections.descendingIterator(); + i.hasNext() && idleConnectionCount > maxIdleConnections; ) { Connection connection = i.next(); if (connection.isIdle()) { expiredConnections.add(connection); @@ -130,16 +134,12 @@ public class ConnectionPool { return systemDefault; } - /** - * Returns total number of connections in the pool. - */ + /** Returns total number of connections in the pool. */ public synchronized int getConnectionCount() { return connections.size(); } - /** - * Returns total number of spdy connections in the pool. - */ + /** Returns total number of spdy connections in the pool. */ public synchronized int getSpdyConnectionCount() { int total = 0; for (Connection connection : connections) { @@ -148,9 +148,7 @@ public class ConnectionPool { return total; } - /** - * Returns total number of http connections in the pool. - */ + /** Returns total number of http connections in the pool. */ public synchronized int getHttpConnectionCount() { int total = 0; for (Connection connection : connections) { diff --git a/src/main/java/com/squareup/okhttp/OkHttpClient.java b/src/main/java/com/squareup/okhttp/OkHttpClient.java index 22054872b..4cc5ec62b 100644 --- a/src/main/java/com/squareup/okhttp/OkHttpClient.java +++ b/src/main/java/com/squareup/okhttp/OkHttpClient.java @@ -27,130 +27,123 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSocketFactory; -/** - * Configures and creates HTTP connections. - */ +/** Configures and creates HTTP connections. */ public final class OkHttpClient { - private Proxy proxy; - private ProxySelector proxySelector; - private CookieHandler cookieHandler; - private ResponseCache responseCache; - private SSLSocketFactory sslSocketFactory; - private HostnameVerifier hostnameVerifier; - private ConnectionPool connectionPool; + private Proxy proxy; + private ProxySelector proxySelector; + private CookieHandler cookieHandler; + private ResponseCache responseCache; + private SSLSocketFactory sslSocketFactory; + private HostnameVerifier hostnameVerifier; + private ConnectionPool connectionPool; - /** - * Sets the HTTP proxy that will be used by connections created by this - * client. This takes precedence over {@link #setProxySelector}, which is - * only honored when this proxy is null (which it is by default). To disable - * proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}. - */ - public OkHttpClient setProxy(Proxy proxy) { - this.proxy = proxy; - return this; - } - - /** - * Sets the proxy selection policy to be used if no {@link #setProxy proxy} - * is specified explicitly. The proxy selector may return multiple proxies; - * in that case they will be tried in sequence until a successful connection - * is established. - * - *

If unset, the {@link ProxySelector#getDefault() system-wide default} - * proxy selector will be used. - */ - public OkHttpClient setProxySelector(ProxySelector proxySelector) { - this.proxySelector = proxySelector; - return this; - } - - /** - * Sets the cookie handler to be used to read outgoing cookies and write - * incoming cookies. - * - *

If unset, the {@link CookieHandler#getDefault() system-wide default} - * cookie handler will be used. - */ - public OkHttpClient setCookieHandler(CookieHandler cookieHandler) { - this.cookieHandler = cookieHandler; - return this; - } - - /** - * Sets the response cache to be used to read and write cached responses. - * - *

If unset, the {@link ResponseCache#getDefault() system-wide default} - * response cache will be used. - */ - public OkHttpClient setResponseCache(ResponseCache responseCache) { - this.responseCache = responseCache; - return this; - } - - /** - * Sets the socket factory used to secure HTTPS connections. - * - *

If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory() - * system-wide default} SSL socket factory will be used. - */ - public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { - this.sslSocketFactory = sslSocketFactory; - return this; - } - - /** - * Sets the verifier used to confirm that response certificates apply to - * requested hostnames for HTTPS connections. - * - *

If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier() - * system-wide default} hostname verifier will be used. - */ - public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) { - this.hostnameVerifier = hostnameVerifier; - return this; - } - - /** - * Sets the connection pool used to recycle HTTP and HTTPS connections. - * - *

If unset, the {@link ConnectionPool#getDefault() system-wide - * default} connection pool will be used. - */ - public OkHttpClient setConnectionPool(ConnectionPool connectionPool) { - this.connectionPool = connectionPool; - return this; - } - - public HttpURLConnection open(URL url) { - ProxySelector proxySelector = this.proxySelector != null - ? this.proxySelector - : ProxySelector.getDefault(); - CookieHandler cookieHandler = this.cookieHandler != null - ? this.cookieHandler - : CookieHandler.getDefault(); - ResponseCache responseCache = this.responseCache != null - ? this.responseCache - : ResponseCache.getDefault(); - ConnectionPool connectionPool = this.connectionPool != null - ? this.connectionPool - : ConnectionPool.getDefault(); - - String protocol = url.getProtocol(); - if (protocol.equals("http")) { - return new HttpURLConnectionImpl( - url, 80, proxy, proxySelector, cookieHandler, responseCache, connectionPool); - } else if (protocol.equals("https")) { - HttpsURLConnectionImpl result = new HttpsURLConnectionImpl( - url, 443, proxy, proxySelector, cookieHandler, responseCache, connectionPool); - result.setSSLSocketFactory(this.sslSocketFactory != null - ? this.sslSocketFactory - : HttpsURLConnection.getDefaultSSLSocketFactory()); - result.setHostnameVerifier(this.hostnameVerifier != null - ? this.hostnameVerifier - : HttpsURLConnection.getDefaultHostnameVerifier()); - return result; - } else { - throw new IllegalArgumentException("Unexpected protocol: " + protocol); - } + /** + * Sets the HTTP proxy that will be used by connections created by this + * client. This takes precedence over {@link #setProxySelector}, which is + * only honored when this proxy is null (which it is by default). To disable + * proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}. + */ + public OkHttpClient setProxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + /** + * Sets the proxy selection policy to be used if no {@link #setProxy proxy} + * is specified explicitly. The proxy selector may return multiple proxies; + * in that case they will be tried in sequence until a successful connection + * is established. + * + *

If unset, the {@link ProxySelector#getDefault() system-wide default} + * proxy selector will be used. + */ + public OkHttpClient setProxySelector(ProxySelector proxySelector) { + this.proxySelector = proxySelector; + return this; + } + + /** + * Sets the cookie handler to be used to read outgoing cookies and write + * incoming cookies. + * + *

If unset, the {@link CookieHandler#getDefault() system-wide default} + * cookie handler will be used. + */ + public OkHttpClient setCookieHandler(CookieHandler cookieHandler) { + this.cookieHandler = cookieHandler; + return this; + } + + /** + * Sets the response cache to be used to read and write cached responses. + * + *

If unset, the {@link ResponseCache#getDefault() system-wide default} + * response cache will be used. + */ + public OkHttpClient setResponseCache(ResponseCache responseCache) { + this.responseCache = responseCache; + return this; + } + + /** + * Sets the socket factory used to secure HTTPS connections. + * + *

If unset, the {@link HttpsURLConnection#getDefaultSSLSocketFactory() + * system-wide default} SSL socket factory will be used. + */ + public OkHttpClient setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + return this; + } + + /** + * Sets the verifier used to confirm that response certificates apply to + * requested hostnames for HTTPS connections. + * + *

If unset, the {@link HttpsURLConnection#getDefaultHostnameVerifier() + * system-wide default} hostname verifier will be used. + */ + public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) { + this.hostnameVerifier = hostnameVerifier; + return this; + } + + /** + * Sets the connection pool used to recycle HTTP and HTTPS connections. + * + *

If unset, the {@link ConnectionPool#getDefault() system-wide + * default} connection pool will be used. + */ + public OkHttpClient setConnectionPool(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + return this; + } + + public HttpURLConnection open(URL url) { + ProxySelector proxySelector = + this.proxySelector != null ? this.proxySelector : ProxySelector.getDefault(); + CookieHandler cookieHandler = + this.cookieHandler != null ? this.cookieHandler : CookieHandler.getDefault(); + ResponseCache responseCache = + this.responseCache != null ? this.responseCache : ResponseCache.getDefault(); + ConnectionPool connectionPool = + this.connectionPool != null ? this.connectionPool : ConnectionPool.getDefault(); + + String protocol = url.getProtocol(); + if (protocol.equals("http")) { + return new HttpURLConnectionImpl(url, 80, proxy, proxySelector, cookieHandler, responseCache, + connectionPool); + } else if (protocol.equals("https")) { + HttpsURLConnectionImpl result = + new HttpsURLConnectionImpl(url, 443, proxy, proxySelector, cookieHandler, responseCache, + connectionPool); + result.setSSLSocketFactory(this.sslSocketFactory != null ? this.sslSocketFactory + : HttpsURLConnection.getDefaultSSLSocketFactory()); + result.setHostnameVerifier(this.hostnameVerifier != null ? this.hostnameVerifier + : HttpsURLConnection.getDefaultHostnameVerifier()); + return result; + } else { + throw new IllegalArgumentException("Unexpected protocol: " + protocol); } + } } diff --git a/src/main/java/com/squareup/okhttp/OkResponseCache.java b/src/main/java/com/squareup/okhttp/OkResponseCache.java index c0f7c5654..b7e3801dc 100644 --- a/src/main/java/com/squareup/okhttp/OkResponseCache.java +++ b/src/main/java/com/squareup/okhttp/OkResponseCache.java @@ -26,19 +26,13 @@ import java.net.HttpURLConnection; */ public interface OkResponseCache { - /** - * Track an HTTP response being satisfied by {@code source}. - */ - void trackResponse(ResponseSource source); + /** Track an HTTP response being satisfied by {@code source}. */ + void trackResponse(ResponseSource source); - /** - * Track an conditional GET that was satisfied by this cache. - */ - void trackConditionalCacheHit(); + /** Track an conditional GET that was satisfied by this cache. */ + void trackConditionalCacheHit(); - /** - * Updates stored HTTP headers using a hit on a conditional GET. - */ - void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection) - throws IOException; + /** Updates stored HTTP headers using a hit on a conditional GET. */ + void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection) + throws IOException; } diff --git a/src/main/java/com/squareup/okhttp/ResponseSource.java b/src/main/java/com/squareup/okhttp/ResponseSource.java index 83388d637..4eca172d3 100644 --- a/src/main/java/com/squareup/okhttp/ResponseSource.java +++ b/src/main/java/com/squareup/okhttp/ResponseSource.java @@ -15,29 +15,23 @@ */ package com.squareup.okhttp; -/** - * The source of an HTTP response. - */ +/** The source of an HTTP response. */ public enum ResponseSource { - /** - * The response was returned from the local cache. - */ - CACHE, + /** The response was returned from the local cache. */ + CACHE, - /** - * The response is available in the cache but must be validated with the - * network. The cache result will be used if it is still valid; otherwise - * the network's response will be used. - */ - CONDITIONAL_CACHE, + /** + * The response is available in the cache but must be validated with the + * network. The cache result will be used if it is still valid; otherwise + * the network's response will be used. + */ + CONDITIONAL_CACHE, - /** - * The response was returned from the network. - */ - NETWORK; + /** The response was returned from the network. */ + NETWORK; - public boolean requiresConnection() { - return this == CONDITIONAL_CACHE || this == NETWORK; - } + public boolean requiresConnection() { + return this == CONDITIONAL_CACHE || this == NETWORK; + } } diff --git a/src/main/java/com/squareup/okhttp/TunnelRequest.java b/src/main/java/com/squareup/okhttp/TunnelRequest.java index 39a820c2b..5260b87c4 100644 --- a/src/main/java/com/squareup/okhttp/TunnelRequest.java +++ b/src/main/java/com/squareup/okhttp/TunnelRequest.java @@ -15,9 +15,10 @@ */ package com.squareup.okhttp; -import static com.squareup.okhttp.internal.Util.getDefaultPort; import com.squareup.okhttp.internal.http.RawHeaders; +import static com.squareup.okhttp.internal.Util.getDefaultPort; + /** * Routing and authentication information sent to an HTTP proxy to create a * HTTPS to an origin server. Everything in the tunnel request is sent @@ -27,48 +28,48 @@ import com.squareup.okhttp.internal.http.RawHeaders; * 5.2. */ public final class TunnelRequest { - final String host; - final int port; - final String userAgent; - final String proxyAuthorization; + final String host; + final int port; + final String userAgent; + final String proxyAuthorization; - /** - * @param host the origin server's hostname. Not null. - * @param port the origin server's port, like 80 or 443. - * @param userAgent the client's user-agent. Not null. - * @param proxyAuthorization proxy authorization, or null if the proxy is - * used without an authorization header. - */ - public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) { - if (host == null) throw new NullPointerException("host == null"); - if (userAgent == null) throw new NullPointerException("userAgent == null"); - this.host = host; - this.port = port; - this.userAgent = userAgent; - this.proxyAuthorization = proxyAuthorization; + /** + * @param host the origin server's hostname. Not null. + * @param port the origin server's port, like 80 or 443. + * @param userAgent the client's user-agent. Not null. + * @param proxyAuthorization proxy authorization, or null if the proxy is + * used without an authorization header. + */ + public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) { + if (host == null) throw new NullPointerException("host == null"); + if (userAgent == null) throw new NullPointerException("userAgent == null"); + this.host = host; + this.port = port; + this.userAgent = userAgent; + this.proxyAuthorization = proxyAuthorization; + } + + /** + * If we're creating a TLS tunnel, send only the minimum set of headers. + * This avoids sending potentially sensitive data like HTTP cookies to + * the proxy unencrypted. + */ + RawHeaders getRequestHeaders() { + RawHeaders result = new RawHeaders(); + result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1"); + + // Always set Host and User-Agent. + result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port)); + result.set("User-Agent", userAgent); + + // Copy over the Proxy-Authorization header if it exists. + if (proxyAuthorization != null) { + result.set("Proxy-Authorization", proxyAuthorization); } - /** - * If we're creating a TLS tunnel, send only the minimum set of headers. - * This avoids sending potentially sensitive data like HTTP cookies to - * the proxy unencrypted. - */ - RawHeaders getRequestHeaders() { - RawHeaders result = new RawHeaders(); - result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1"); - - // Always set Host and User-Agent. - result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port)); - result.set("User-Agent", userAgent); - - // Copy over the Proxy-Authorization header if it exists. - if (proxyAuthorization != null) { - result.set("Proxy-Authorization", proxyAuthorization); - } - - // Always set the Proxy-Connection to Keep-Alive for the benefit of - // HTTP/1.0 proxies like Squid. - result.set("Proxy-Connection", "Keep-Alive"); - return result; - } + // Always set the Proxy-Connection to Keep-Alive for the benefit of + // HTTP/1.0 proxies like Squid. + result.set("Proxy-Connection", "Keep-Alive"); + return result; + } } diff --git a/src/main/java/com/squareup/okhttp/internal/Base64.java b/src/main/java/com/squareup/okhttp/internal/Base64.java index 458e536d8..46681035c 100644 --- a/src/main/java/com/squareup/okhttp/internal/Base64.java +++ b/src/main/java/com/squareup/okhttp/internal/Base64.java @@ -16,150 +16,148 @@ */ /** -* @author Alexander Y. Kleymenov -*/ + * @author Alexander Y. Kleymenov + */ package com.squareup.okhttp.internal; -import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY; import java.io.UnsupportedEncodingException; +import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY; + /** * Base64 encoder/decoder. * In violation of the RFC, this encoder doesn't wrap lines at 76 columns. */ public final class Base64 { - private Base64() { - } + private Base64() { + } - public static byte[] decode(byte[] in) { - return decode(in, in.length); - } + public static byte[] decode(byte[] in) { + return decode(in, in.length); + } - public static byte[] decode(byte[] in, int len) { - // approximate output length - int length = len / 4 * 3; - // return an empty array on empty or short input without padding - if (length == 0) { - return EMPTY_BYTE_ARRAY; - } - // temporary array - byte[] out = new byte[length]; - // number of padding characters ('=') - int pad = 0; - byte chr; - // compute the number of the padding characters - // and adjust the length of the input - for (;; len--) { - chr = in[len - 1]; - // skip the neutral characters - if ((chr == '\n') || (chr == '\r') - || (chr == ' ') || (chr == '\t')) { - continue; - } - if (chr == '=') { - pad++; - } else { - break; - } - } - // index in the output array - int outIndex = 0; - // index in the input array - int inIndex = 0; - // holds the value of the input character - int bits = 0; - // holds the value of the input quantum - int quantum = 0; - for (int i = 0; i < len; i++) { - chr = in[i]; - // skip the neutral characters - if ((chr == '\n') || (chr == '\r') - || (chr == ' ') || (chr == '\t')) { - continue; - } - if ((chr >= 'A') && (chr <= 'Z')) { - // char ASCII value - // A 65 0 - // Z 90 25 (ASCII - 65) - bits = chr - 65; - } else if ((chr >= 'a') && (chr <= 'z')) { - // char ASCII value - // a 97 26 - // z 122 51 (ASCII - 71) - bits = chr - 71; - } else if ((chr >= '0') && (chr <= '9')) { - // char ASCII value - // 0 48 52 - // 9 57 61 (ASCII + 4) - bits = chr + 4; - } else if (chr == '+') { - bits = 62; - } else if (chr == '/') { - bits = 63; - } else { - return null; - } - // append the value to the quantum - quantum = (quantum << 6) | (byte) bits; - if (inIndex % 4 == 3) { - // 4 characters were read, so make the output: - out[outIndex++] = (byte) (quantum >> 16); - out[outIndex++] = (byte) (quantum >> 8); - out[outIndex++] = (byte) quantum; - } - inIndex++; - } - if (pad > 0) { - // adjust the quantum value according to the padding - quantum = quantum << (6 * pad); - // make output - out[outIndex++] = (byte) (quantum >> 16); - if (pad == 1) { - out[outIndex++] = (byte) (quantum >> 8); - } - } - // create the resulting array - byte[] result = new byte[outIndex]; - System.arraycopy(out, 0, result, 0, outIndex); - return result; + public static byte[] decode(byte[] in, int len) { + // approximate output length + int length = len / 4 * 3; + // return an empty array on empty or short input without padding + if (length == 0) { + return EMPTY_BYTE_ARRAY; } - - private static final byte[] MAP = new byte[] - {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', - 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', - 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', - 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', - '4', '5', '6', '7', '8', '9', '+', '/'}; - - public static String encode(byte[] in) { - int length = (in.length + 2) * 4 / 3; - byte[] out = new byte[length]; - int index = 0, end = in.length - in.length % 3; - for (int i = 0; i < end; i += 3) { - out[index++] = MAP[(in[i] & 0xff) >> 2]; - out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; - out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; - out[index++] = MAP[(in[i + 2] & 0x3f)]; - } - switch (in.length % 3) { - case 1: - out[index++] = MAP[(in[end] & 0xff) >> 2]; - out[index++] = MAP[(in[end] & 0x03) << 4]; - out[index++] = '='; - out[index++] = '='; - break; - case 2: - out[index++] = MAP[(in[end] & 0xff) >> 2]; - out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; - out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; - out[index++] = '='; - break; - } - try { - return new String(out, 0, index, "US-ASCII"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } + // temporary array + byte[] out = new byte[length]; + // number of padding characters ('=') + int pad = 0; + byte chr; + // compute the number of the padding characters + // and adjust the length of the input + for (; ; len--) { + chr = in[len - 1]; + // skip the neutral characters + if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) { + continue; + } + if (chr == '=') { + pad++; + } else { + break; + } } + // index in the output array + int outIndex = 0; + // index in the input array + int inIndex = 0; + // holds the value of the input character + int bits = 0; + // holds the value of the input quantum + int quantum = 0; + for (int i = 0; i < len; i++) { + chr = in[i]; + // skip the neutral characters + if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) { + continue; + } + if ((chr >= 'A') && (chr <= 'Z')) { + // char ASCII value + // A 65 0 + // Z 90 25 (ASCII - 65) + bits = chr - 65; + } else if ((chr >= 'a') && (chr <= 'z')) { + // char ASCII value + // a 97 26 + // z 122 51 (ASCII - 71) + bits = chr - 71; + } else if ((chr >= '0') && (chr <= '9')) { + // char ASCII value + // 0 48 52 + // 9 57 61 (ASCII + 4) + bits = chr + 4; + } else if (chr == '+') { + bits = 62; + } else if (chr == '/') { + bits = 63; + } else { + return null; + } + // append the value to the quantum + quantum = (quantum << 6) | (byte) bits; + if (inIndex % 4 == 3) { + // 4 characters were read, so make the output: + out[outIndex++] = (byte) (quantum >> 16); + out[outIndex++] = (byte) (quantum >> 8); + out[outIndex++] = (byte) quantum; + } + inIndex++; + } + if (pad > 0) { + // adjust the quantum value according to the padding + quantum = quantum << (6 * pad); + // make output + out[outIndex++] = (byte) (quantum >> 16); + if (pad == 1) { + out[outIndex++] = (byte) (quantum >> 8); + } + } + // create the resulting array + byte[] result = new byte[outIndex]; + System.arraycopy(out, 0, result, 0, outIndex); + return result; + } + + private static final byte[] MAP = + new byte[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', + 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; + + public static String encode(byte[] in) { + int length = (in.length + 2) * 4 / 3; + byte[] out = new byte[length]; + int index = 0, end = in.length - in.length % 3; + for (int i = 0; i < end; i += 3) { + out[index++] = MAP[(in[i] & 0xff) >> 2]; + out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; + out[index++] = MAP[(in[i + 2] & 0x3f)]; + } + switch (in.length % 3) { + case 1: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[(in[end] & 0x03) << 4]; + out[index++] = '='; + out[index++] = '='; + break; + case 2: + out[index++] = MAP[(in[end] & 0xff) >> 2]; + out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; + out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; + out[index++] = '='; + break; + } + try { + return new String(out, 0, index, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java index 96f6d96fa..dfb6f61d0 100644 --- a/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java +++ b/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java @@ -16,7 +16,6 @@ package com.squareup.okhttp.internal; -import static com.squareup.okhttp.internal.Util.UTF_8; import java.io.BufferedWriter; import java.io.Closeable; import java.io.EOFException; @@ -43,6 +42,8 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import static com.squareup.okhttp.internal.Util.UTF_8; + /** * A cache that uses a bounded amount of space on a filesystem. Each cache * entry has a string key and a fixed number of values. Values are byte @@ -65,12 +66,12 @@ import java.util.concurrent.TimeUnit; * entry may have only one editor at one time; if a value is not available to be * edited then {@link #edit} will return null. *

* Every {@link #edit} call must be matched by a call to {@link Editor#commit} * or {@link Editor#abort}. Committing is atomic: a read observes the full set @@ -87,15 +88,15 @@ import java.util.concurrent.TimeUnit; * responding appropriately. */ public final class DiskLruCache implements Closeable { - static final String JOURNAL_FILE = "journal"; - static final String JOURNAL_FILE_TMP = "journal.tmp"; - static final String MAGIC = "libcore.io.DiskLruCache"; - static final String VERSION_1 = "1"; - static final long ANY_SEQUENCE_NUMBER = -1; - private static final String CLEAN = "CLEAN"; - private static final String DIRTY = "DIRTY"; - private static final String REMOVE = "REMOVE"; - private static final String READ = "READ"; + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TMP = "journal.tmp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; /* * This cache uses a journal file named "journal". A typical journal file @@ -137,709 +138,693 @@ public final class DiskLruCache implements Closeable { * it exists when the cache is opened. */ - private final File directory; - private final File journalFile; - private final File journalFileTmp; - private final int appVersion; - private final long maxSize; - private final int valueCount; - private long size = 0; - private Writer journalWriter; - private final LinkedHashMap lruEntries - = new LinkedHashMap(0, 0.75f, true); - private int redundantOpCount; + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final int appVersion; + private final long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries = + new LinkedHashMap(0, 0.75f, true); + private int redundantOpCount; - /** - * To differentiate between old and current snapshots, each entry is given - * a sequence number each time an edit is committed. A snapshot is stale if - * its sequence number is not equal to its entry's sequence number. - */ - private long nextSequenceNumber = 0; + /** + * To differentiate between old and current snapshots, each entry is given + * a sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; - /** This cache uses a single background thread to evict entries. */ - private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, - 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); - private final Callable cleanupCallable = new Callable() { - @Override public Void call() throws Exception { - synchronized (DiskLruCache.this) { - if (journalWriter == null) { - return null; // closed - } - trimToSize(); - if (journalRebuildRequired()) { - rebuildJournal(); - redundantOpCount = 0; - } - } - return null; + /** This cache uses a single background thread to evict entries. */ + private final ExecutorService executorService = + new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + @Override public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // closed } - }; + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; - private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { - this.directory = directory; - this.appVersion = appVersion; - this.journalFile = new File(directory, JOURNAL_FILE); - this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); - this.valueCount = valueCount; - this.maxSize = maxSize; + private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory a writable directory + * @param valueCount the number of values per cache entry. Must be positive. + * @param maxSize the maximum number of bytes this cache should use to store + * @throws IOException if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) + throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); } - /** - * Opens the cache in {@code directory}, creating a cache if none exists - * there. - * - * @param directory a writable directory - * @param appVersion - * @param valueCount the number of values per cache entry. Must be positive. - * @param maxSize the maximum number of bytes this cache should use to store - * @throws IOException if reading or writing the cache directory fails - */ - public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) - throws IOException { - if (maxSize <= 0) { - throw new IllegalArgumentException("maxSize <= 0"); - } - if (valueCount <= 0) { - throw new IllegalArgumentException("valueCount <= 0"); - } - - // prefer to pick up where we left off - DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - if (cache.journalFile.exists()) { - try { - cache.readJournal(); - cache.processJournal(); - cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true)); - return cache; - } catch (IOException journalIsCorrupt) { - Platform.get().logW("DiskLruCache " + directory + " is corrupt: " - + journalIsCorrupt.getMessage() + ", removing"); - cache.delete(); - } - } - - // create a new empty cache - directory.mkdirs(); - cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - cache.rebuildJournal(); + // prefer to pick up where we left off + DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true)); return cache; + } catch (IOException journalIsCorrupt) { + Platform.get() + .logW("DiskLruCache " + + directory + + " is corrupt: " + + journalIsCorrupt.getMessage() + + ", removing"); + cache.delete(); + } } - private void readJournal() throws IOException { - StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), - Util.US_ASCII); + // create a new empty cache + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); + try { + String magic = reader.readLine(); + String version = reader.readLine(); + String appVersionString = reader.readLine(); + String valueCountString = reader.readLine(); + String blank = reader.readLine(); + if (!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion) + .equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !"" + .equals(blank)) { + throw new IOException("unexpected journal header: [" + + magic + + ", " + + version + + ", " + + valueCountString + + ", " + + blank + + "]"); + } + + while (true) { try { - String magic = reader.readLine(); - String version = reader.readLine(); - String appVersionString = reader.readLine(); - String valueCountString = reader.readLine(); - String blank = reader.readLine(); - if (!MAGIC.equals(magic) - || !VERSION_1.equals(version) - || !Integer.toString(appVersion).equals(appVersionString) - || !Integer.toString(valueCount).equals(valueCountString) - || !"".equals(blank)) { - throw new IOException("unexpected journal header: [" - + magic + ", " + version + ", " + valueCountString + ", " + blank + "]"); - } - - while (true) { - try { - readJournalLine(reader.readLine()); - } catch (EOFException endOfJournal) { - break; - } - } - } finally { - Util.closeQuietly(reader); + readJournalLine(reader.readLine()); + } catch (EOFException endOfJournal) { + break; } + } + } finally { + Util.closeQuietly(reader); + } + } + + private void readJournalLine(String line) throws IOException { + String[] parts = line.split(" "); + if (parts.length < 2) { + throw new IOException("unexpected journal line: " + line); } - private void readJournalLine(String line) throws IOException { - String[] parts = line.split(" "); - if (parts.length < 2) { - throw new IOException("unexpected journal line: " + line); - } - - String key = parts[1]; - if (parts[0].equals(REMOVE) && parts.length == 2) { - lruEntries.remove(key); - return; - } - - Entry entry = lruEntries.get(key); - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } - - if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { - entry.readable = true; - entry.currentEditor = null; - entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length)); - } else if (parts[0].equals(DIRTY) && parts.length == 2) { - entry.currentEditor = new Editor(entry); - } else if (parts[0].equals(READ) && parts.length == 2) { - // this work was already done by calling lruEntries.get() - } else { - throw new IOException("unexpected journal line: " + line); - } + String key = parts[1]; + if (parts[0].equals(REMOVE) && parts.length == 2) { + lruEntries.remove(key); + return; } - /** - * Computes the initial size and collects garbage as a part of opening the - * cache. Dirty entries are assumed to be inconsistent and will be deleted. - */ - private void processJournal() throws IOException { - deleteIfExists(journalFileTmp); - for (Iterator i = lruEntries.values().iterator(); i.hasNext();) { - Entry entry = i.next(); - if (entry.currentEditor == null) { - for (int t = 0; t < valueCount; t++) { - size += entry.lengths[t]; - } - } else { - entry.currentEditor = null; - for (int t = 0; t < valueCount; t++) { - deleteIfExists(entry.getCleanFile(t)); - deleteIfExists(entry.getDirtyFile(t)); - } - i.remove(); - } - } + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); } - /** - * Creates a new journal that omits redundant information. This replaces the - * current journal if it exists. - */ - private synchronized void rebuildJournal() throws IOException { - if (journalWriter != null) { - journalWriter.close(); + if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) { + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(Arrays.copyOfRange(parts, 2, parts.length)); + } else if (parts[0].equals(DIRTY) && parts.length == 2) { + entry.currentEditor = new Editor(entry); + } else if (parts[0].equals(READ) && parts.length == 2) { + // this work was already done by calling lruEntries.get() + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; } - - Writer writer = new BufferedWriter(new FileWriter(journalFileTmp)); - writer.write(MAGIC); - writer.write("\n"); - writer.write(VERSION_1); - writer.write("\n"); - writer.write(Integer.toString(appVersion)); - writer.write("\n"); - writer.write(Integer.toString(valueCount)); - writer.write("\n"); - writer.write("\n"); - - for (Entry entry : lruEntries.values()) { - if (entry.currentEditor != null) { - writer.write(DIRTY + ' ' + entry.key + '\n'); - } else { - writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); } + i.remove(); + } + } + } - writer.close(); - journalFileTmp.renameTo(journalFile); - journalWriter = new BufferedWriter(new FileWriter(journalFile, true)); + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); } - private static void deleteIfExists(File file) throws IOException { - file.delete(); + Writer writer = new BufferedWriter(new FileWriter(journalFileTmp)); + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + } } - /** - * Returns a snapshot of the entry named {@code key}, or null if it doesn't - * exist is not currently readable. If a value is returned, it is moved to - * the head of the LRU queue. - */ - public synchronized Snapshot get(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null) { - return null; - } + writer.close(); + journalFileTmp.renameTo(journalFile); + journalWriter = new BufferedWriter(new FileWriter(journalFile, true)); + } - if (!entry.readable) { - return null; - } + private static void deleteIfExists(File file) throws IOException { + file.delete(); + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } /* * Open all streams eagerly to guarantee that we see a single published * snapshot. If we opened streams lazily then the streams could come * from different edits. */ - InputStream[] ins = new InputStream[valueCount]; + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // a file must have been deleted manually! + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null + || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // snapshot is stale + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // another edit is in progress + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // flush the journal before creating files to prevent file leaks + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** Returns the directory where this cache stores its data. */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public long maxSize() { + return maxSize; + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // if this edit is creating the entry for the first time, every index must have a value + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!editor.written[i]) { + editor.abort(); + throw new IllegalStateException("Newly created entry didn't create value for index " + i); + } + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + Platform.get().logW("DiskLruCache: Newly created entry doesn't have file for index " + i); + return; + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int redundantOpCompactThreshold = 2000; + return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (!file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** Returns true if this cache has been closed. */ + public boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { + if (journalWriter == null) { + throw new IllegalStateException("cache is closed"); + } + } + + /** Force buffered operations to the filesystem. */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** Closes this cache. Stored values will remain on the filesystem. */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // already closed + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } + } + trimToSize(); + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator().next(); + remove(toEvict.getKey()); + } + } + + /** + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. + */ + public void delete() throws IOException { + close(); + Util.deleteContents(directory); + } + + private void validateKey(String key) { + if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { + throw new IllegalArgumentException( + "keys must not contain spaces or newlines: \"" + key + "\""); + } + } + + private static String inputStreamToString(InputStream in) throws IOException { + return Util.readFully(new InputStreamReader(in, UTF_8)); + } + + /** A snapshot of the values for an entry. */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + } + + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); + } + + /** Returns the unbuffered stream with the value for {@code index}. */ + public InputStream getInputStream(int index) { + return ins[index]; + } + + /** Returns the string value for {@code index}. */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); + } + + @Override public void close() { + for (InputStream in : ins) { + Util.closeQuietly(in); + } + } + } + + /** Edits the values for an entry. */ + public final class Editor { + private final Entry entry; + private final boolean[] written; + private boolean hasErrors; + + private Editor(Entry entry) { + this.entry = entry; + this.written = (entry.readable) ? null : new boolean[valueCount]; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + return new FileInputStream(entry.getCleanFile(index)); + } + } + + /** + * Returns the last committed value as a string, or null if no value + * has been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors + * when writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + written[index] = true; + } + return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); + } + } + + /** Sets the value at {@code index} to {@code value}. */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), UTF_8); + writer.write(value); + } finally { + Util.closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the + * edit lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // the previous entry is stale + } else { + completeEdit(this, true); + } + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + private final class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override public void write(int oneByte) { try { - for (int i = 0; i < valueCount; i++) { - ins[i] = new FileInputStream(entry.getCleanFile(i)); - } - } catch (FileNotFoundException e) { - // a file must have been deleted manually! - return null; + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; } + } - redundantOpCount++; - journalWriter.append(READ + ' ' + key + '\n'); - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); + @Override public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; } + } - return new Snapshot(key, entry.sequenceNumber, ins); + @Override public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } + } + } + + private final class Entry { + private final String key; + + /** Lengths of this entry's files. */ + private final long[] lengths; + + /** True if this entry has ever been published. */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** The sequence number of the most recently committed edit to this entry. */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; } - /** - * Returns an editor for the entry named {@code key}, or null if another - * edit is in progress. - */ - public Editor edit(String key) throws IOException { - return edit(key, ANY_SEQUENCE_NUMBER); + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); } - private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER - && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { - return null; // snapshot is stale - } - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } else if (entry.currentEditor != null) { - return null; // another edit is in progress - } + /** Set lengths using decimal numbers like "10123". */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } - Editor editor = new Editor(entry); - entry.currentEditor = editor; - - // flush the journal before creating files to prevent file leaks - journalWriter.write(DIRTY + ' ' + key + '\n'); - journalWriter.flush(); - return editor; + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } } - /** - * Returns the directory where this cache stores its data. - */ - public File getDirectory() { - return directory; + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + Arrays.toString(strings)); } - /** - * Returns the maximum number of bytes that this cache should use to store - * its data. - */ - public long maxSize() { - return maxSize; + public File getCleanFile(int i) { + return new File(directory, key + "." + i); } - /** - * Returns the number of bytes currently being used to store the values in - * this cache. This may be greater than the max size if a background - * deletion is pending. - */ - public synchronized long size() { - return size; - } - - private synchronized void completeEdit(Editor editor, boolean success) throws IOException { - Entry entry = editor.entry; - if (entry.currentEditor != editor) { - throw new IllegalStateException(); - } - - // if this edit is creating the entry for the first time, every index must have a value - if (success && !entry.readable) { - for (int i = 0; i < valueCount; i++) { - if (!editor.written[i]) { - editor.abort(); - throw new IllegalStateException( - "Newly created entry didn't create value for index " + i); - } - if (!entry.getDirtyFile(i).exists()) { - editor.abort(); - Platform.get().logW( - "DiskLruCache: Newly created entry doesn't have file for index " + i); - return; - } - } - } - - for (int i = 0; i < valueCount; i++) { - File dirty = entry.getDirtyFile(i); - if (success) { - if (dirty.exists()) { - File clean = entry.getCleanFile(i); - dirty.renameTo(clean); - long oldLength = entry.lengths[i]; - long newLength = clean.length(); - entry.lengths[i] = newLength; - size = size - oldLength + newLength; - } - } else { - deleteIfExists(dirty); - } - } - - redundantOpCount++; - entry.currentEditor = null; - if (entry.readable | success) { - entry.readable = true; - journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - if (success) { - entry.sequenceNumber = nextSequenceNumber++; - } - } else { - lruEntries.remove(entry.key); - journalWriter.write(REMOVE + ' ' + entry.key + '\n'); - } - - if (size > maxSize || journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - } - - /** - * We only rebuild the journal when it will halve the size of the journal - * and eliminate at least 2000 ops. - */ - private boolean journalRebuildRequired() { - final int redundantOpCompactThreshold = 2000; - return redundantOpCount >= redundantOpCompactThreshold - && redundantOpCount >= lruEntries.size(); - } - - /** - * Drops the entry for {@code key} if it exists and can be removed. Entries - * actively being edited cannot be removed. - * - * @return true if an entry was removed. - */ - public synchronized boolean remove(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null || entry.currentEditor != null) { - return false; - } - - for (int i = 0; i < valueCount; i++) { - File file = entry.getCleanFile(i); - if (!file.delete()) { - throw new IOException("failed to delete " + file); - } - size -= entry.lengths[i]; - entry.lengths[i] = 0; - } - - redundantOpCount++; - journalWriter.append(REMOVE + ' ' + key + '\n'); - lruEntries.remove(key); - - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return true; - } - - /** - * Returns true if this cache has been closed. - */ - public boolean isClosed() { - return journalWriter == null; - } - - private void checkNotClosed() { - if (journalWriter == null) { - throw new IllegalStateException("cache is closed"); - } - } - - /** - * Force buffered operations to the filesystem. - */ - public synchronized void flush() throws IOException { - checkNotClosed(); - trimToSize(); - journalWriter.flush(); - } - - /** - * Closes this cache. Stored values will remain on the filesystem. - */ - public synchronized void close() throws IOException { - if (journalWriter == null) { - return; // already closed - } - for (Entry entry : new ArrayList(lruEntries.values())) { - if (entry.currentEditor != null) { - entry.currentEditor.abort(); - } - } - trimToSize(); - journalWriter.close(); - journalWriter = null; - } - - private void trimToSize() throws IOException { - while (size > maxSize) { - Map.Entry toEvict = lruEntries.entrySet().iterator().next(); - remove(toEvict.getKey()); - } - } - - /** - * Closes the cache and deletes all of its stored values. This will delete - * all files in the cache directory including files that weren't created by - * the cache. - */ - public void delete() throws IOException { - close(); - Util.deleteContents(directory); - } - - private void validateKey(String key) { - if (key.contains(" ") || key.contains("\n") || key.contains("\r")) { - throw new IllegalArgumentException( - "keys must not contain spaces or newlines: \"" + key + "\""); - } - } - - private static String inputStreamToString(InputStream in) throws IOException { - return Util.readFully(new InputStreamReader(in, UTF_8)); - } - - /** - * A snapshot of the values for an entry. - */ - public final class Snapshot implements Closeable { - private final String key; - private final long sequenceNumber; - private final InputStream[] ins; - - private Snapshot(String key, long sequenceNumber, InputStream[] ins) { - this.key = key; - this.sequenceNumber = sequenceNumber; - this.ins = ins; - } - - /** - * Returns an editor for this snapshot's entry, or null if either the - * entry has changed since this snapshot was created or if another edit - * is in progress. - */ - public Editor edit() throws IOException { - return DiskLruCache.this.edit(key, sequenceNumber); - } - - /** - * Returns the unbuffered stream with the value for {@code index}. - */ - public InputStream getInputStream(int index) { - return ins[index]; - } - - /** - * Returns the string value for {@code index}. - */ - public String getString(int index) throws IOException { - return inputStreamToString(getInputStream(index)); - } - - @Override public void close() { - for (InputStream in : ins) { - Util.closeQuietly(in); - } - } - } - - /** - * Edits the values for an entry. - */ - public final class Editor { - private final Entry entry; - private final boolean[] written; - private boolean hasErrors; - - private Editor(Entry entry) { - this.entry = entry; - this.written = (entry.readable) ? null : new boolean[valueCount]; - } - - /** - * Returns an unbuffered input stream to read the last committed value, - * or null if no value has been committed. - */ - public InputStream newInputStream(int index) throws IOException { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); - } - if (!entry.readable) { - return null; - } - return new FileInputStream(entry.getCleanFile(index)); - } - } - - /** - * Returns the last committed value as a string, or null if no value - * has been committed. - */ - public String getString(int index) throws IOException { - InputStream in = newInputStream(index); - return in != null ? inputStreamToString(in) : null; - } - - /** - * Returns a new unbuffered output stream to write the value at - * {@code index}. If the underlying output stream encounters errors - * when writing to the filesystem, this edit will be aborted when - * {@link #commit} is called. The returned output stream does not throw - * IOExceptions. - */ - public OutputStream newOutputStream(int index) throws IOException { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); - } - if (!entry.readable) { - written[index] = true; - } - return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index))); - } - } - - /** - * Sets the value at {@code index} to {@code value}. - */ - public void set(int index, String value) throws IOException { - Writer writer = null; - try { - writer = new OutputStreamWriter(newOutputStream(index), UTF_8); - writer.write(value); - } finally { - Util.closeQuietly(writer); - } - } - - /** - * Commits this edit so it is visible to readers. This releases the - * edit lock so another edit may be started on the same key. - */ - public void commit() throws IOException { - if (hasErrors) { - completeEdit(this, false); - remove(entry.key); // the previous entry is stale - } else { - completeEdit(this, true); - } - } - - /** - * Aborts this edit. This releases the edit lock so another edit may be - * started on the same key. - */ - public void abort() throws IOException { - completeEdit(this, false); - } - - private final class FaultHidingOutputStream extends FilterOutputStream { - private FaultHidingOutputStream(OutputStream out) { - super(out); - } - - @Override public void write(int oneByte) { - try { - out.write(oneByte); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override public void write(byte[] buffer, int offset, int length) { - try { - out.write(buffer, offset, length); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override public void close() { - try { - out.close(); - } catch (IOException e) { - hasErrors = true; - } - } - - @Override public void flush() { - try { - out.flush(); - } catch (IOException e) { - hasErrors = true; - } - } - } - } - - private final class Entry { - private final String key; - - /** Lengths of this entry's files. */ - private final long[] lengths; - - /** True if this entry has ever been published. */ - private boolean readable; - - /** The ongoing edit or null if this entry is not being edited. */ - private Editor currentEditor; - - /** The sequence number of the most recently committed edit to this entry. */ - private long sequenceNumber; - - private Entry(String key) { - this.key = key; - this.lengths = new long[valueCount]; - } - - public String getLengths() throws IOException { - StringBuilder result = new StringBuilder(); - for (long size : lengths) { - result.append(' ').append(size); - } - return result.toString(); - } - - /** - * Set lengths using decimal numbers like "10123". - */ - private void setLengths(String[] strings) throws IOException { - if (strings.length != valueCount) { - throw invalidLengths(strings); - } - - try { - for (int i = 0; i < strings.length; i++) { - lengths[i] = Long.parseLong(strings[i]); - } - } catch (NumberFormatException e) { - throw invalidLengths(strings); - } - } - - private IOException invalidLengths(String[] strings) throws IOException { - throw new IOException("unexpected journal line: " + Arrays.toString(strings)); - } - - public File getCleanFile(int i) { - return new File(directory, key + "." + i); - } - - public File getDirtyFile(int i) { - return new File(directory, key + "." + i + ".tmp"); - } + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/Dns.java b/src/main/java/com/squareup/okhttp/internal/Dns.java index 37fa6092e..69b2d37e9 100644 --- a/src/main/java/com/squareup/okhttp/internal/Dns.java +++ b/src/main/java/com/squareup/okhttp/internal/Dns.java @@ -23,11 +23,11 @@ import java.net.UnknownHostException; * make code more testable. */ public interface Dns { - Dns DEFAULT = new Dns() { - @Override public InetAddress[] getAllByName(String host) throws UnknownHostException { - return InetAddress.getAllByName(host); - } - }; + Dns DEFAULT = new Dns() { + @Override public InetAddress[] getAllByName(String host) throws UnknownHostException { + return InetAddress.getAllByName(host); + } + }; - InetAddress[] getAllByName(String host) throws UnknownHostException; + InetAddress[] getAllByName(String host) throws UnknownHostException; } diff --git a/src/main/java/com/squareup/okhttp/internal/Platform.java b/src/main/java/com/squareup/okhttp/internal/Platform.java index b647df230..f6c57a814 100644 --- a/src/main/java/com/squareup/okhttp/internal/Platform.java +++ b/src/main/java/com/squareup/okhttp/internal/Platform.java @@ -47,309 +47,298 @@ import javax.net.ssl.SSLSocket; * public API in Java 7 and callable via reflection in Android 4.1+. */ public class Platform { - private static final Platform PLATFORM = findPlatform(); + private static final Platform PLATFORM = findPlatform(); - private Constructor deflaterConstructor; + private Constructor deflaterConstructor; - public static Platform get() { - return PLATFORM; + public static Platform get() { + return PLATFORM; + } + + public void logW(String warning) { + System.out.println(warning); + } + + public void tagSocket(Socket socket) throws SocketException { + } + + public void untagSocket(Socket socket) throws SocketException { + } + + public URI toUriLenient(URL url) throws URISyntaxException { + return url.toURI(); // this isn't as good as the built-in toUriLenient + } + + /** + * Attempt a TLS connection with useful extensions enabled. This mode + * supports more features, but is less likely to be compatible with older + * HTTPS servers. + */ + public void enableTlsExtensions(SSLSocket socket, String uriHost) { + } + + /** + * Attempt a secure connection with basic functionality to maximize + * compatibility. Currently this uses SSL 3.0. + */ + public void supportTlsIntolerantServer(SSLSocket socket) { + socket.setEnabledProtocols(new String[] { "SSLv3" }); + } + + /** Returns the negotiated protocol, or null if no protocol was negotiated. */ + public byte[] getNpnSelectedProtocol(SSLSocket socket) { + return null; + } + + /** + * Sets client-supported protocols on a socket to send to a server. The + * protocols are only sent if the socket implementation supports NPN. + */ + public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { + } + + /** + * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name + * value blocks. This throws an {@link UnsupportedOperationException} on + * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH. + */ + public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater, + boolean syncFlush) { + try { + Constructor constructor = deflaterConstructor; + if (constructor == null) { + constructor = deflaterConstructor = + DeflaterOutputStream.class.getConstructor(OutputStream.class, Deflater.class, + boolean.class); + } + return constructor.newInstance(out, deflater, syncFlush); + } catch (NoSuchMethodException e) { + throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available"); + } catch (InvocationTargetException e) { + throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause() + : new RuntimeException(e.getCause()); + } catch (InstantiationException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + + /** Attempt to match the host runtime to a capable Platform implementation. */ + private static Platform findPlatform() { + // Attempt to find Android 2.3+ APIs. + Class openSslSocketClass; + Method setUseSessionTickets; + Method setHostname; + try { + openSslSocketClass = Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); + setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class); + setHostname = openSslSocketClass.getMethod("setHostname", String.class); + + // Attempt to find Android 4.1+ APIs. + try { + Method setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class); + Method getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol"); + return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols, + getNpnSelectedProtocol); + } catch (NoSuchMethodException ignored) { + return new Android23(openSslSocketClass, setUseSessionTickets, setHostname); + } + } catch (ClassNotFoundException ignored) { + // This isn't an Android runtime. + } catch (NoSuchMethodException ignored) { + // This isn't Android 2.3 or better. } - public void logW(String warning) { - System.out.println(warning); + // Attempt to find the Jetty's NPN extension for OpenJDK. + try { + String npnClassName = "org.eclipse.jetty.npn.NextProtoNego"; + Class nextProtoNegoClass = Class.forName(npnClassName); + Class providerClass = Class.forName(npnClassName + "$Provider"); + Class clientProviderClass = Class.forName(npnClassName + "$ClientProvider"); + Class serverProviderClass = Class.forName(npnClassName + "$ServerProvider"); + Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass); + Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class); + return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass, + serverProviderClass); + } catch (ClassNotFoundException ignored) { + return new Platform(); // NPN isn't on the classpath. + } catch (NoSuchMethodException ignored) { + return new Platform(); // The NPN version isn't what we expect. + } + } + + /** + * Android version 2.3 and newer support TLS session tickets and server name + * indication (SNI). + */ + private static class Android23 extends Platform { + protected final Class openSslSocketClass; + private final Method setUseSessionTickets; + private final Method setHostname; + + private Android23(Class openSslSocketClass, Method setUseSessionTickets, + Method setHostname) { + this.openSslSocketClass = openSslSocketClass; + this.setUseSessionTickets = setUseSessionTickets; + this.setHostname = setHostname; } - public void tagSocket(Socket socket) throws SocketException { - } - - public void untagSocket(Socket socket) throws SocketException { - } - - public URI toUriLenient(URL url) throws URISyntaxException { - return url.toURI(); // this isn't as good as the built-in toUriLenient - } - - /** - * Attempt a TLS connection with useful extensions enabled. This mode - * supports more features, but is less likely to be compatible with older - * HTTPS servers. - */ - public void enableTlsExtensions(SSLSocket socket, String uriHost) { - } - - /** - * Attempt a secure connection with basic functionality to maximize - * compatibility. Currently this uses SSL 3.0. - */ - public void supportTlsIntolerantServer(SSLSocket socket) { - socket.setEnabledProtocols(new String[]{"SSLv3"}); - } - - /** - * Returns the negotiated protocol, or null if no protocol was negotiated. - */ - public byte[] getNpnSelectedProtocol(SSLSocket socket) { - return null; - } - - /** - * Sets client-supported protocols on a socket to send to a server. The - * protocols are only sent if the socket implementation supports NPN. - */ - public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { - } - - /** - * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name - * value blocks. This throws an {@link UnsupportedOperationException} on - * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH. - */ - public OutputStream newDeflaterOutputStream( - OutputStream out, Deflater deflater, boolean syncFlush) { + @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) { + super.enableTlsExtensions(socket, uriHost); + if (openSslSocketClass.isInstance(socket)) { + // This is Android: use reflection on OpenSslSocketImpl. try { - Constructor constructor = deflaterConstructor; - if (constructor == null) { - constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor( - OutputStream.class, Deflater.class, boolean.class); - } - return constructor.newInstance(out, deflater, syncFlush); - } catch (NoSuchMethodException e) { - throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available"); + setUseSessionTickets.invoke(socket, true); + setHostname.invoke(socket, uriHost); } catch (InvocationTargetException e) { - throw e.getCause() instanceof RuntimeException - ? (RuntimeException) e.getCause() - : new RuntimeException(e.getCause()); - } catch (InstantiationException e) { - throw new RuntimeException(e); + throw new RuntimeException(e); } catch (IllegalAccessException e) { - throw new AssertionError(); + throw new AssertionError(e); } + } + } + } + + /** Android version 4.1 and newer support NPN. */ + private static class Android41 extends Android23 { + private final Method setNpnProtocols; + private final Method getNpnSelectedProtocol; + + private Android41(Class openSslSocketClass, Method setUseSessionTickets, Method setHostname, + Method setNpnProtocols, Method getNpnSelectedProtocol) { + super(openSslSocketClass, setUseSessionTickets, setHostname); + this.setNpnProtocols = setNpnProtocols; + this.getNpnSelectedProtocol = getNpnSelectedProtocol; } - /** - * Attempt to match the host runtime to a capable Platform implementation. - */ - private static Platform findPlatform() { - // Attempt to find Android 2.3+ APIs. - Class openSslSocketClass; - Method setUseSessionTickets; - Method setHostname; - try { - openSslSocketClass = Class.forName( - "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl"); - setUseSessionTickets = openSslSocketClass.getMethod( - "setUseSessionTickets", boolean.class); - setHostname = openSslSocketClass.getMethod("setHostname", String.class); - - // Attempt to find Android 4.1+ APIs. - try { - Method setNpnProtocols = openSslSocketClass.getMethod( - "setNpnProtocols", byte[].class); - Method getNpnSelectedProtocol = openSslSocketClass.getMethod( - "getNpnSelectedProtocol"); - return new Android41(openSslSocketClass, setUseSessionTickets, setHostname, - setNpnProtocols, getNpnSelectedProtocol); - } catch (NoSuchMethodException ignored) { - return new Android23(openSslSocketClass, setUseSessionTickets, setHostname); - } - } catch (ClassNotFoundException ignored) { - // This isn't an Android runtime. - } catch (NoSuchMethodException ignored) { - // This isn't Android 2.3 or better. - } - - // Attempt to find the Jetty's NPN extension for OpenJDK. - try { - String npnClassName = "org.eclipse.jetty.npn.NextProtoNego"; - Class nextProtoNegoClass = Class.forName(npnClassName); - Class providerClass = Class.forName(npnClassName + "$Provider"); - Class clientProviderClass = Class.forName(npnClassName + "$ClientProvider"); - Class serverProviderClass = Class.forName(npnClassName + "$ServerProvider"); - Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass); - Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class); - return new JdkWithJettyNpnPlatform(putMethod, getMethod, - clientProviderClass, serverProviderClass); - } catch (ClassNotFoundException ignored) { - return new Platform(); // NPN isn't on the classpath. - } catch (NoSuchMethodException ignored) { - return new Platform(); // The NPN version isn't what we expect. - } + @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { + if (!openSslSocketClass.isInstance(socket)) { + return; + } + try { + setNpnProtocols.invoke(socket, new Object[] { npnProtocols }); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } } - /** - * Android version 2.3 and newer support TLS session tickets and server name - * indication (SNI). - */ - private static class Android23 extends Platform { - protected final Class openSslSocketClass; - private final Method setUseSessionTickets; - private final Method setHostname; + @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) { + if (!openSslSocketClass.isInstance(socket)) { + return null; + } + try { + return (byte[]) getNpnSelectedProtocol.invoke(socket); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + } - private Android23(Class openSslSocketClass, Method setUseSessionTickets, - Method setHostname) { - this.openSslSocketClass = openSslSocketClass; - this.setUseSessionTickets = setUseSessionTickets; - this.setHostname = setHostname; - } + /** + * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class + * path. + */ + private static class JdkWithJettyNpnPlatform extends Platform { + private final Method getMethod; + private final Method putMethod; + private final Class clientProviderClass; + private final Class serverProviderClass; - @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) { - super.enableTlsExtensions(socket, uriHost); - if (openSslSocketClass.isInstance(socket)) { - // This is Android: use reflection on OpenSslSocketImpl. - try { - setUseSessionTickets.invoke(socket, true); - setHostname.invoke(socket, uriHost); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - } - } + public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class clientProviderClass, + Class serverProviderClass) { + this.putMethod = putMethod; + this.getMethod = getMethod; + this.clientProviderClass = clientProviderClass; + this.serverProviderClass = serverProviderClass; } - /** - * Android version 4.1 and newer support NPN. - */ - private static class Android41 extends Android23 { - private final Method setNpnProtocols; - private final Method getNpnSelectedProtocol; - - private Android41(Class openSslSocketClass, Method setUseSessionTickets, - Method setHostname, Method setNpnProtocols, Method getNpnSelectedProtocol) { - super(openSslSocketClass, setUseSessionTickets, setHostname); - this.setNpnProtocols = setNpnProtocols; - this.getNpnSelectedProtocol = getNpnSelectedProtocol; - } - - @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { - if (!openSslSocketClass.isInstance(socket)) { - return; - } - try { - setNpnProtocols.invoke(socket, new Object[] {npnProtocols}); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } - } - - @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) { - if (!openSslSocketClass.isInstance(socket)) { - return null; - } - try { - return (byte[]) getNpnSelectedProtocol.invoke(socket); - } catch (InvocationTargetException e) { - throw new RuntimeException(e); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } + @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { + try { + List strings = new ArrayList(); + for (int i = 0; i < npnProtocols.length; ) { + int length = npnProtocols[i++]; + strings.add(new String(npnProtocols, i, length, "US-ASCII")); + i += length; } + Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(), + new Class[] { clientProviderClass, serverProviderClass }, + new JettyNpnProvider(strings)); + putMethod.invoke(null, socket, provider); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } catch (InvocationTargetException e) { + throw new AssertionError(e); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } } - /** - * OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class - * path. - */ - private static class JdkWithJettyNpnPlatform extends Platform { - private final Method getMethod; - private final Method putMethod; - private final Class clientProviderClass; - private final Class serverProviderClass; - - public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, - Class clientProviderClass, Class serverProviderClass) { - this.putMethod = putMethod; - this.getMethod = getMethod; - this.clientProviderClass = clientProviderClass; - this.serverProviderClass = serverProviderClass; + @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) { + try { + JettyNpnProvider provider = + (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket)); + if (!provider.unsupported && provider.selected == null) { + Logger logger = Logger.getLogger(OkHttpClient.class.getName()); + logger.log(Level.INFO, + "NPN callback dropped so SPDY is disabled. " + "Is npn-boot on the boot class path?"); + return null; } + return provider.unsupported ? null : provider.selected.getBytes("US-ASCII"); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(); + } catch (InvocationTargetException e) { + throw new AssertionError(); + } catch (IllegalAccessException e) { + throw new AssertionError(); + } + } + } - @Override public void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) { - try { - List strings = new ArrayList(); - for (int i = 0; i < npnProtocols.length;) { - int length = npnProtocols[i++]; - strings.add(new String(npnProtocols, i, length, "US-ASCII")); - i += length; - } - Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(), - new Class[] {clientProviderClass, serverProviderClass}, - new JettyNpnProvider(strings)); - putMethod.invoke(null, socket, provider); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } catch (InvocationTargetException e) { - throw new AssertionError(e); - } catch (IllegalAccessException e) { - throw new AssertionError(e); - } - } + /** + * Handle the methods of NextProtoNego's ClientProvider and ServerProvider + * without a compile-time dependency on those interfaces. + */ + private static class JettyNpnProvider implements InvocationHandler { + private final List protocols; + private boolean unsupported; + private String selected; - @Override public byte[] getNpnSelectedProtocol(SSLSocket socket) { - try { - JettyNpnProvider provider = (JettyNpnProvider) Proxy.getInvocationHandler( - getMethod.invoke(null, socket)); - if (!provider.unsupported && provider.selected == null) { - Logger logger = Logger.getLogger(OkHttpClient.class.getName()); - logger.log(Level.INFO, "NPN callback dropped so SPDY is disabled. " - + "Is npn-boot on the boot class path?"); - return null; - } - return provider.unsupported - ? null - : provider.selected.getBytes("US-ASCII"); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(); - } catch (InvocationTargetException e) { - throw new AssertionError(); - } catch (IllegalAccessException e) { - throw new AssertionError(); - } - } + public JettyNpnProvider(List protocols) { + this.protocols = protocols; } - /** - * Handle the methods of NextProtoNego's ClientProvider and ServerProvider - * without a compile-time dependency on those interfaces. - */ - private static class JettyNpnProvider implements InvocationHandler { - private final List protocols; - private boolean unsupported; - private String selected; - - public JettyNpnProvider(List protocols) { - this.protocols = protocols; - } - - @Override public Object invoke(Object proxy, Method method, Object[] args) - throws Throwable { - String methodName = method.getName(); - Class returnType = method.getReturnType(); - if (args == null) { - args = Util.EMPTY_STRING_ARRAY; - } - if (methodName.equals("supports") && boolean.class == returnType) { - return true; - } else if (methodName.equals("unsupported") && void.class == returnType) { - this.unsupported = true; - return null; - } else if (methodName.equals("protocols") && args.length == 0) { - return protocols; - } else if (methodName.equals("selectProtocol") && String.class == returnType - && args.length == 1 && (args[0] == null || args[0] instanceof List)) { - // TODO: use OpenSSL's algorithm which uses both lists - List serverProtocols = (List) args[0]; - this.selected = protocols.get(0); - return selected; - } else if (methodName.equals("protocolSelected") && args.length == 1) { - this.selected = (String) args[0]; - return null; - } else { - return method.invoke(this, args); - } - } + @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + Class returnType = method.getReturnType(); + if (args == null) { + args = Util.EMPTY_STRING_ARRAY; + } + if (methodName.equals("supports") && boolean.class == returnType) { + return true; + } else if (methodName.equals("unsupported") && void.class == returnType) { + this.unsupported = true; + return null; + } else if (methodName.equals("protocols") && args.length == 0) { + return protocols; + } else if (methodName.equals("selectProtocol") + && String.class == returnType + && args.length == 1 + && (args[0] == null || args[0] instanceof List)) { + // TODO: use OpenSSL's algorithm which uses both lists + List serverProtocols = (List) args[0]; + this.selected = protocols.get(0); + return selected; + } else if (methodName.equals("protocolSelected") && args.length == 1) { + this.selected = (String) args[0]; + return null; + } else { + return method.invoke(this, args); + } } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java index 9011b2c6a..4d158a0b3 100644 --- a/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java +++ b/src/main/java/com/squareup/okhttp/internal/StrictLineReader.java @@ -16,9 +16,6 @@ package com.squareup.okhttp.internal; -import static com.squareup.okhttp.internal.Util.ISO_8859_1; -import static com.squareup.okhttp.internal.Util.US_ASCII; -import static com.squareup.okhttp.internal.Util.UTF_8; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.EOFException; @@ -26,6 +23,10 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import static com.squareup.okhttp.internal.Util.ISO_8859_1; +import static com.squareup.okhttp.internal.Util.US_ASCII; +import static com.squareup.okhttp.internal.Util.UTF_8; + /** * Buffers input from an {@link InputStream} for reading lines. * @@ -45,188 +46,188 @@ import java.nio.charset.Charset; * The default charset is US_ASCII. */ public class StrictLineReader implements Closeable { - private static final byte CR = (byte) '\r'; - private static final byte LF = (byte) '\n'; + private static final byte CR = (byte) '\r'; + private static final byte LF = (byte) '\n'; - private final InputStream in; - private final Charset charset; + private final InputStream in; + private final Charset charset; - /* - * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end - * and the data in the range [pos, end) is buffered for reading. At end of input, if there is - * an unterminated line, we set end == -1, otherwise end == pos. If the underlying - * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. - */ - private byte[] buf; - private int pos; - private int end; + /* + * Buffered data is stored in {@code buf}. As long as no exception occurs, 0 <= pos <= end + * and the data in the range [pos, end) is buffered for reading. At end of input, if there is + * an unterminated line, we set end == -1, otherwise end == pos. If the underlying + * {@code InputStream} throws an {@code IOException}, end may remain as either pos or -1. + */ + private byte[] buf; + private int pos; + private int end; - /** - * Constructs a new {@code StrictLineReader} with the default capacity and charset. - * - * @param in the {@code InputStream} to read data from. - * @throws NullPointerException if {@code in} is null. - */ - public StrictLineReader(InputStream in) { - this(in, 8192); + /** + * Constructs a new {@code StrictLineReader} with the default capacity and charset. + * + * @param in the {@code InputStream} to read data from. + * @throws NullPointerException if {@code in} is null. + */ + public StrictLineReader(InputStream in) { + this(in, 8192); + } + + /** + * Constructs a new {@code LineReader} with the specified capacity and the default charset. + * + * @param in the {@code InputStream} to read data from. + * @param capacity the capacity of the buffer. + * @throws NullPointerException if {@code in} is null. + * @throws IllegalArgumentException for negative or zero {@code capacity}. + */ + public StrictLineReader(InputStream in, int capacity) { + this(in, capacity, US_ASCII); + } + + /** + * Constructs a new {@code LineReader} with the specified charset and the default capacity. + * + * @param in the {@code InputStream} to read data from. + * @param charset the charset used to decode data. + * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if the specified charset is not supported. + */ + public StrictLineReader(InputStream in, Charset charset) { + this(in, 8192, charset); + } + + /** + * Constructs a new {@code LineReader} with the specified capacity and charset. + * + * @param in the {@code InputStream} to read data from. + * @param capacity the capacity of the buffer. + * @param charset the charset used to decode data. + * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. + * @throws NullPointerException if {@code in} or {@code charset} is null. + * @throws IllegalArgumentException if {@code capacity} is negative or zero + * or the specified charset is not supported. + */ + public StrictLineReader(InputStream in, int capacity, Charset charset) { + if (in == null || charset == null) { + throw new NullPointerException(); + } + if (capacity < 0) { + throw new IllegalArgumentException("capacity <= 0"); + } + if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) { + throw new IllegalArgumentException("Unsupported encoding"); } - /** - * Constructs a new {@code LineReader} with the specified capacity and the default charset. - * - * @param in the {@code InputStream} to read data from. - * @param capacity the capacity of the buffer. - * @throws NullPointerException if {@code in} is null. - * @throws IllegalArgumentException for negative or zero {@code capacity}. - */ - public StrictLineReader(InputStream in, int capacity) { - this(in, capacity, US_ASCII); - } + this.in = in; + this.charset = charset; + buf = new byte[capacity]; + } - /** - * Constructs a new {@code LineReader} with the specified charset and the default capacity. - * - * @param in the {@code InputStream} to read data from. - * @param charset the charset used to decode data. - * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. - * @throws NullPointerException if {@code in} or {@code charset} is null. - * @throws IllegalArgumentException if the specified charset is not supported. - */ - public StrictLineReader(InputStream in, Charset charset) { - this(in, 8192, charset); + /** + * Closes the reader by closing the underlying {@code InputStream} and + * marking this reader as closed. + * + * @throws IOException for errors when closing the underlying {@code InputStream}. + */ + @Override + public void close() throws IOException { + synchronized (in) { + if (buf != null) { + buf = null; + in.close(); + } } + } - /** - * Constructs a new {@code LineReader} with the specified capacity and charset. - * - * @param in the {@code InputStream} to read data from. - * @param capacity the capacity of the buffer. - * @param charset the charset used to decode data. - * Only US-ASCII, UTF-8 and ISO-8859-1 is supported. - * @throws NullPointerException if {@code in} or {@code charset} is null. - * @throws IllegalArgumentException if {@code capacity} is negative or zero - * or the specified charset is not supported. - */ - public StrictLineReader(InputStream in, int capacity, Charset charset) { - if (in == null || charset == null) { - throw new NullPointerException(); - } - if (capacity < 0) { - throw new IllegalArgumentException("capacity <= 0"); - } - if (!(charset.equals(US_ASCII) || charset.equals(UTF_8) || charset.equals(ISO_8859_1))) { - throw new IllegalArgumentException("Unsupported encoding"); + /** + * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, + * this end of line marker is not included in the result. + * + * @return the next line from the input. + * @throws IOException for underlying {@code InputStream} errors. + * @throws EOFException for the end of source stream. + */ + public String readLine() throws IOException { + synchronized (in) { + if (buf == null) { + throw new IOException("LineReader is closed"); + } + + // Read more data if we are at the end of the buffered data. + // Though it's an error to read after an exception, we will let {@code fillBuf()} + // throw again if that happens; thus we need to handle end == -1 as well as end == pos. + if (pos >= end) { + fillBuf(); + } + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; + String res = new String(buf, pos, lineEnd - pos, charset); + pos = i + 1; + return res; } + } - this.in = in; - this.charset = charset; - buf = new byte[capacity]; - } + // Let's anticipate up to 80 characters on top of those already read. + ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { + @Override + public String toString() { + int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; + return new String(buf, 0, length, charset); + } + }; - /** - * Closes the reader by closing the underlying {@code InputStream} and - * marking this reader as closed. - * - * @throws IOException for errors when closing the underlying {@code InputStream}. - */ - @Override - public void close() throws IOException { - synchronized (in) { - if (buf != null) { - buf = null; - in.close(); + while (true) { + out.write(buf, pos, end - pos); + // Mark unterminated line in case fillBuf throws EOFException or IOException. + end = -1; + fillBuf(); + // Try to find LF in the buffered data and return the line if successful. + for (int i = pos; i != end; ++i) { + if (buf[i] == LF) { + if (i != pos) { + out.write(buf, pos, i - pos); } + pos = i + 1; + return out.toString(); + } } + } } + } - /** - * Reads the next line. A line ends with {@code "\n"} or {@code "\r\n"}, - * this end of line marker is not included in the result. - * - * @return the next line from the input. - * @throws IOException for underlying {@code InputStream} errors. - * @throws EOFException for the end of source stream. - */ - public String readLine() throws IOException { - synchronized (in) { - if (buf == null) { - throw new IOException("LineReader is closed"); - } - - // Read more data if we are at the end of the buffered data. - // Though it's an error to read after an exception, we will let {@code fillBuf()} - // throw again if that happens; thus we need to handle end == -1 as well as end == pos. - if (pos >= end) { - fillBuf(); - } - // Try to find LF in the buffered data and return the line if successful. - for (int i = pos; i != end; ++i) { - if (buf[i] == LF) { - int lineEnd = (i != pos && buf[i - 1] == CR) ? i - 1 : i; - String res = new String(buf, pos, lineEnd - pos, charset); - pos = i + 1; - return res; - } - } - - // Let's anticipate up to 80 characters on top of those already read. - ByteArrayOutputStream out = new ByteArrayOutputStream(end - pos + 80) { - @Override - public String toString() { - int length = (count > 0 && buf[count - 1] == CR) ? count - 1 : count; - return new String(buf, 0, length, charset); - } - }; - - while (true) { - out.write(buf, pos, end - pos); - // Mark unterminated line in case fillBuf throws EOFException or IOException. - end = -1; - fillBuf(); - // Try to find LF in the buffered data and return the line if successful. - for (int i = pos; i != end; ++i) { - if (buf[i] == LF) { - if (i != pos) { - out.write(buf, pos, i - pos); - } - pos = i + 1; - return out.toString(); - } - } - } - } + /** + * Read an {@code int} from a line containing its decimal representation. + * + * @return the value of the {@code int} from the next line. + * @throws IOException for underlying {@code InputStream} errors or conversion error. + * @throws EOFException for the end of source stream. + */ + public int readInt() throws IOException { + String intString = readLine(); + try { + return Integer.parseInt(intString); + } catch (NumberFormatException e) { + throw new IOException("expected an int but was \"" + intString + "\""); } + } - /** - * Read an {@code int} from a line containing its decimal representation. - * - * @return the value of the {@code int} from the next line. - * @throws IOException for underlying {@code InputStream} errors or conversion error. - * @throws EOFException for the end of source stream. - */ - public int readInt() throws IOException { - String intString = readLine(); - try { - return Integer.parseInt(intString); - } catch (NumberFormatException e) { - throw new IOException("expected an int but was \"" + intString + "\""); - } - } - - /** - * Reads new input data into the buffer. Call only with pos == end or end == -1, - * depending on the desired outcome if the function throws. - * - * @throws IOException for underlying {@code InputStream} errors. - * @throws EOFException for the end of source stream. - */ - private void fillBuf() throws IOException { - int result = in.read(buf, 0, buf.length); - if (result == -1) { - throw new EOFException(); - } - pos = 0; - end = result; + /** + * Reads new input data into the buffer. Call only with pos == end or end == -1, + * depending on the desired outcome if the function throws. + * + * @throws IOException for underlying {@code InputStream} errors. + * @throws EOFException for the end of source stream. + */ + private void fillBuf() throws IOException { + int result = in.read(buf, 0, buf.length); + if (result == -1) { + throw new EOFException(); } + pos = 0; + end = result; + } } diff --git a/src/main/java/com/squareup/okhttp/internal/Util.java b/src/main/java/com/squareup/okhttp/internal/Util.java index 530b7af71..7f6a0388f 100644 --- a/src/main/java/com/squareup/okhttp/internal/Util.java +++ b/src/main/java/com/squareup/okhttp/internal/Util.java @@ -32,314 +32,303 @@ import java.nio.charset.Charset; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicReference; -/** - * Junk drawer of utility methods. - */ +/** Junk drawer of utility methods. */ public final class Util { - public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - public static final String[] EMPTY_STRING_ARRAY = new String[0]; + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + public static final String[] EMPTY_STRING_ARRAY = new String[0]; - /** A cheap and type-safe constant for the ISO-8859-1 Charset. */ - public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); + /** A cheap and type-safe constant for the ISO-8859-1 Charset. */ + public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); - /** A cheap and type-safe constant for the US-ASCII Charset. */ - public static final Charset US_ASCII = Charset.forName("US-ASCII"); + /** A cheap and type-safe constant for the US-ASCII Charset. */ + public static final Charset US_ASCII = Charset.forName("US-ASCII"); - /** A cheap and type-safe constant for the UTF-8 Charset. */ - public static final Charset UTF_8 = Charset.forName("UTF-8"); - private static AtomicReference skipBuffer = new AtomicReference(); + /** A cheap and type-safe constant for the UTF-8 Charset. */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + private static AtomicReference skipBuffer = new AtomicReference(); - private Util() { + private Util() { + } + + public static int getEffectivePort(URI uri) { + return getEffectivePort(uri.getScheme(), uri.getPort()); + } + + public static int getEffectivePort(URL url) { + return getEffectivePort(url.getProtocol(), url.getPort()); + } + + private static int getEffectivePort(String scheme, int specifiedPort) { + return specifiedPort != -1 ? specifiedPort : getDefaultPort(scheme); + } + + public static int getDefaultPort(String scheme) { + if ("http".equalsIgnoreCase(scheme)) { + return 80; + } else if ("https".equalsIgnoreCase(scheme)) { + return 443; + } else { + return -1; + } + } + + public static void checkOffsetAndCount(int arrayLength, int offset, int count) { + if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { + throw new ArrayIndexOutOfBoundsException(); + } + } + + public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) { + if (order == ByteOrder.BIG_ENDIAN) { + dst[offset++] = (byte) ((value >> 24) & 0xff); + dst[offset++] = (byte) ((value >> 16) & 0xff); + dst[offset++] = (byte) ((value >> 8) & 0xff); + dst[offset] = (byte) ((value >> 0) & 0xff); + } else { + dst[offset++] = (byte) ((value >> 0) & 0xff); + dst[offset++] = (byte) ((value >> 8) & 0xff); + dst[offset++] = (byte) ((value >> 16) & 0xff); + dst[offset] = (byte) ((value >> 24) & 0xff); + } + } + + /** Returns true if two possibly-null objects are equal. */ + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Closes {@code closeable}, ignoring any checked exceptions. Does nothing + * if {@code closeable} is null. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code socket}, ignoring any checked exceptions. Does nothing if + * {@code socket} is null. + */ + public static void closeQuietly(Socket socket) { + if (socket != null) { + try { + socket.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } + + /** + * Closes {@code a} and {@code b}. If either close fails, this completes + * the other close and rethrows the first encountered exception. + */ + public static void closeAll(Closeable a, Closeable b) throws IOException { + Throwable thrown = null; + try { + a.close(); + } catch (Throwable e) { + thrown = e; + } + try { + b.close(); + } catch (Throwable e) { + if (thrown == null) thrown = e; + } + if (thrown == null) return; + if (thrown instanceof IOException) throw (IOException) thrown; + if (thrown instanceof RuntimeException) throw (RuntimeException) thrown; + if (thrown instanceof Error) throw (Error) thrown; + throw new AssertionError(thrown); + } + + /** Recursively delete everything in {@code dir}. */ + // TODO: this should specify paths as Strings rather than as Files + public static void deleteContents(File dir) throws IOException { + File[] files = dir.listFiles(); + if (files == null) { + throw new IllegalArgumentException("not a directory: " + dir); + } + for (File file : files) { + if (file.isDirectory()) { + deleteContents(file); + } + if (!file.delete()) { + throw new IOException("failed to delete file: " + file); + } + } + } + + /** + * Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int). + * InputStream assumes that you implement InputStream.read(int) and provides default + * implementations of the others, but often the opposite is more efficient. + */ + public static int readSingleByte(InputStream in) throws IOException { + byte[] buffer = new byte[1]; + int result = in.read(buffer, 0, 1); + return (result != -1) ? buffer[0] & 0xff : -1; + } + + /** + * Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int). + * OutputStream assumes that you implement OutputStream.write(int) and provides default + * implementations of the others, but often the opposite is more efficient. + */ + public static void writeSingleByte(OutputStream out, int b) throws IOException { + byte[] buffer = new byte[1]; + buffer[0] = (byte) (b & 0xff); + out.write(buffer); + } + + /** Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available. */ + public static void readFully(InputStream in, byte[] dst) throws IOException { + readFully(in, dst, 0, dst.length); + } + + /** + * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws + * EOFException if insufficient bytes are available. + * + * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}. + */ + public static void readFully(InputStream in, byte[] dst, int offset, int byteCount) + throws IOException { + if (byteCount == 0) { + return; + } + if (in == null) { + throw new NullPointerException("in == null"); + } + if (dst == null) { + throw new NullPointerException("dst == null"); + } + checkOffsetAndCount(dst.length, offset, byteCount); + while (byteCount > 0) { + int bytesRead = in.read(dst, offset, byteCount); + if (bytesRead < 0) { + throw new EOFException(); + } + offset += bytesRead; + byteCount -= bytesRead; + } + } + + /** Returns the remainder of 'reader' as a string, closing it when done. */ + public static String readFully(Reader reader) throws IOException { + try { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); + } + return writer.toString(); + } finally { + reader.close(); + } + } + + public static void skipAll(InputStream in) throws IOException { + do { + in.skip(Long.MAX_VALUE); + } while (in.read() != -1); + } + + /** + * Call {@code in.read()} repeatedly until either the stream is exhausted or + * {@code byteCount} bytes have been read. + * + *

This method reuses the skip buffer but is careful to never use it at + * the same time that another stream is using it. Otherwise streams that use + * the caller's buffer for consistency checks like CRC could be clobbered by + * other threads. A thread-local buffer is also insufficient because some + * streams may call other streams in their skip() method, also clobbering the + * buffer. + */ + public static long skipByReading(InputStream in, long byteCount) throws IOException { + // acquire the shared skip buffer. + byte[] buffer = skipBuffer.getAndSet(null); + if (buffer == null) { + buffer = new byte[4096]; } - public static int getEffectivePort(URI uri) { - return getEffectivePort(uri.getScheme(), uri.getPort()); + long skipped = 0; + while (skipped < byteCount) { + int toRead = (int) Math.min(byteCount - skipped, buffer.length); + int read = in.read(buffer, 0, toRead); + if (read == -1) { + break; + } + skipped += read; + if (read < toRead) { + break; + } } - public static int getEffectivePort(URL url) { - return getEffectivePort(url.getProtocol(), url.getPort()); + // release the shared skip buffer. + skipBuffer.set(buffer); + + return skipped; + } + + /** + * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed. + * Returns the total number of bytes transferred. + */ + public static int copy(InputStream in, OutputStream out) throws IOException { + int total = 0; + byte[] buffer = new byte[8192]; + int c; + while ((c = in.read(buffer)) != -1) { + total += c; + out.write(buffer, 0, c); } + return total; + } - private static int getEffectivePort(String scheme, int specifiedPort) { - return specifiedPort != -1 - ? specifiedPort - : getDefaultPort(scheme); + /** + * Returns the ASCII characters up to but not including the next "\r\n", or + * "\n". + * + * @throws java.io.EOFException if the stream is exhausted before the next newline + * character. + */ + public static String readAsciiLine(InputStream in) throws IOException { + // TODO: support UTF-8 here instead + StringBuilder result = new StringBuilder(80); + while (true) { + int c = in.read(); + if (c == -1) { + throw new EOFException(); + } else if (c == '\n') { + break; + } + + result.append((char) c); } - - public static int getDefaultPort(String scheme) { - if ("http".equalsIgnoreCase(scheme)) { - return 80; - } else if ("https".equalsIgnoreCase(scheme)) { - return 443; - } else { - return -1; - } + int length = result.length(); + if (length > 0 && result.charAt(length - 1) == '\r') { + result.setLength(length - 1); } + return result.toString(); + } - public static void checkOffsetAndCount(int arrayLength, int offset, int count) { - if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) { - throw new ArrayIndexOutOfBoundsException(); - } - } - - public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) { - if (order == ByteOrder.BIG_ENDIAN) { - dst[offset++] = (byte) ((value >> 24) & 0xff); - dst[offset++] = (byte) ((value >> 16) & 0xff); - dst[offset++] = (byte) ((value >> 8) & 0xff); - dst[offset ] = (byte) ((value >> 0) & 0xff); - } else { - dst[offset++] = (byte) ((value >> 0) & 0xff); - dst[offset++] = (byte) ((value >> 8) & 0xff); - dst[offset++] = (byte) ((value >> 16) & 0xff); - dst[offset ] = (byte) ((value >> 24) & 0xff); - } - } - - /** - * Returns true if two possibly-null objects are equal. - */ - public static boolean equal(Object a, Object b) { - return a == b || (a != null && a.equals(b)); - } - - /** - * Closes {@code closeable}, ignoring any checked exceptions. Does nothing - * if {@code closeable} is null. - */ - public static void closeQuietly(Closeable closeable) { - if (closeable != null) { - try { - closeable.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - - /** - * Closes {@code socket}, ignoring any checked exceptions. Does nothing if - * {@code socket} is null. - */ - public static void closeQuietly(Socket socket) { - if (socket != null) { - try { - socket.close(); - } catch (RuntimeException rethrown) { - throw rethrown; - } catch (Exception ignored) { - } - } - } - - /** - * Closes {@code a} and {@code b}. If either close fails, this completes - * the other close and rethrows the first encountered exception. - */ - public static void closeAll(Closeable a, Closeable b) throws IOException { - Throwable thrown = null; - try { - a.close(); - } catch (Throwable e) { - thrown = e; - } - try { - b.close(); - } catch (Throwable e) { - if (thrown == null) thrown = e; - } - if (thrown == null) return; - if (thrown instanceof IOException) throw (IOException) thrown; - if (thrown instanceof RuntimeException) throw (RuntimeException) thrown; - if (thrown instanceof Error) throw (Error) thrown; - throw new AssertionError(thrown); - } - - /** - * Recursively delete everything in {@code dir}. - */ - // TODO: this should specify paths as Strings rather than as Files - public static void deleteContents(File dir) throws IOException { - File[] files = dir.listFiles(); - if (files == null) { - throw new IllegalArgumentException("not a directory: " + dir); - } - for (File file : files) { - if (file.isDirectory()) { - deleteContents(file); - } - if (!file.delete()) { - throw new IOException("failed to delete file: " + file); - } - } - } - - /** - * Implements InputStream.read(int) in terms of InputStream.read(byte[], int, int). - * InputStream assumes that you implement InputStream.read(int) and provides default - * implementations of the others, but often the opposite is more efficient. - */ - public static int readSingleByte(InputStream in) throws IOException { - byte[] buffer = new byte[1]; - int result = in.read(buffer, 0, 1); - return (result != -1) ? buffer[0] & 0xff : -1; - } - - /** - * Implements OutputStream.write(int) in terms of OutputStream.write(byte[], int, int). - * OutputStream assumes that you implement OutputStream.write(int) and provides default - * implementations of the others, but often the opposite is more efficient. - */ - public static void writeSingleByte(OutputStream out, int b) throws IOException { - byte[] buffer = new byte[1]; - buffer[0] = (byte) (b & 0xff); - out.write(buffer); - } - - /** - * Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available. - */ - public static void readFully(InputStream in, byte[] dst) throws IOException { - readFully(in, dst, 0, dst.length); - } - - /** - * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws - * EOFException if insufficient bytes are available. - * - * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}. - */ - public static void readFully(InputStream in, byte[] dst, int offset, int byteCount) throws IOException { - if (byteCount == 0) { - return; - } - if (in == null) { - throw new NullPointerException("in == null"); - } - if (dst == null) { - throw new NullPointerException("dst == null"); - } - checkOffsetAndCount(dst.length, offset, byteCount); - while (byteCount > 0) { - int bytesRead = in.read(dst, offset, byteCount); - if (bytesRead < 0) { - throw new EOFException(); - } - offset += bytesRead; - byteCount -= bytesRead; - } - } - - /** - * Returns the remainder of 'reader' as a string, closing it when done. - */ - public static String readFully(Reader reader) throws IOException { - try { - StringWriter writer = new StringWriter(); - char[] buffer = new char[1024]; - int count; - while ((count = reader.read(buffer)) != -1) { - writer.write(buffer, 0, count); - } - return writer.toString(); - } finally { - reader.close(); - } - } - - public static void skipAll(InputStream in) throws IOException { - do { - in.skip(Long.MAX_VALUE); - } while (in.read() != -1); - } - - /** - * Call {@code in.read()} repeatedly until either the stream is exhausted or - * {@code byteCount} bytes have been read. - * - *

This method reuses the skip buffer but is careful to never use it at - * the same time that another stream is using it. Otherwise streams that use - * the caller's buffer for consistency checks like CRC could be clobbered by - * other threads. A thread-local buffer is also insufficient because some - * streams may call other streams in their skip() method, also clobbering the - * buffer. - */ - public static long skipByReading(InputStream in, long byteCount) throws IOException { - // acquire the shared skip buffer. - byte[] buffer = skipBuffer.getAndSet(null); - if (buffer == null) { - buffer = new byte[4096]; - } - - long skipped = 0; - while (skipped < byteCount) { - int toRead = (int) Math.min(byteCount - skipped, buffer.length); - int read = in.read(buffer, 0, toRead); - if (read == -1) { - break; - } - skipped += read; - if (read < toRead) { - break; - } - } - - // release the shared skip buffer. - skipBuffer.set(buffer); - - return skipped; - } - - /** - * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed. - * Returns the total number of bytes transferred. - */ - public static int copy(InputStream in, OutputStream out) throws IOException { - int total = 0; - byte[] buffer = new byte[8192]; - int c; - while ((c = in.read(buffer)) != -1) { - total += c; - out.write(buffer, 0, c); - } - return total; - } - - /** - * Returns the ASCII characters up to but not including the next "\r\n", or - * "\n". - * - * @throws java.io.EOFException if the stream is exhausted before the next newline - * character. - */ - public static String readAsciiLine(InputStream in) throws IOException { - // TODO: support UTF-8 here instead - StringBuilder result = new StringBuilder(80); - while (true) { - int c = in.read(); - if (c == -1) { - throw new EOFException(); - } else if (c == '\n') { - break; - } - - result.append((char) c); - } - int length = result.length(); - if (length > 0 && result.charAt(length - 1) == '\r') { - result.setLength(length - 1); - } - return result.toString(); - } - - public static ThreadFactory newThreadFactory(final String name, final boolean daemon) { - return new ThreadFactory() { - @Override public Thread newThread(Runnable r) { - Thread result = new Thread(r, name); - result.setDaemon(daemon); - return result; - } - }; - } + public static ThreadFactory newThreadFactory(final String name, final boolean daemon) { + return new ThreadFactory() { + @Override public Thread newThread(Runnable r) { + Thread result = new Thread(r, name); + result.setDaemon(daemon); + return result; + } + }; + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java index 254c03d5a..187f3b6e1 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java +++ b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpInputStream.java @@ -33,75 +33,75 @@ import java.net.CacheRequest; * invalidated. */ abstract class AbstractHttpInputStream extends InputStream { - protected final InputStream in; - protected final HttpEngine httpEngine; - private final CacheRequest cacheRequest; - private final OutputStream cacheBody; - protected boolean closed; + protected final InputStream in; + protected final HttpEngine httpEngine; + private final CacheRequest cacheRequest; + private final OutputStream cacheBody; + protected boolean closed; - AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, - CacheRequest cacheRequest) throws IOException { - this.in = in; - this.httpEngine = httpEngine; + AbstractHttpInputStream(InputStream in, HttpEngine httpEngine, CacheRequest cacheRequest) + throws IOException { + this.in = in; + this.httpEngine = httpEngine; - OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null; + OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null; - // some apps return a null body; for compatibility we treat that like a null cache request - if (cacheBody == null) { - cacheRequest = null; - } - - this.cacheBody = cacheBody; - this.cacheRequest = cacheRequest; + // some apps return a null body; for compatibility we treat that like a null cache request + if (cacheBody == null) { + cacheRequest = null; } - /** - * read() is implemented using read(byte[], int, int) so subclasses only - * need to override the latter. - */ - @Override public final int read() throws IOException { - return Util.readSingleByte(this); - } + this.cacheBody = cacheBody; + this.cacheRequest = cacheRequest; + } - protected final void checkNotClosed() throws IOException { - if (closed) { - throw new IOException("stream closed"); - } - } + /** + * read() is implemented using read(byte[], int, int) so subclasses only + * need to override the latter. + */ + @Override public final int read() throws IOException { + return Util.readSingleByte(this); + } - protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException { - if (cacheBody != null) { - cacheBody.write(buffer, offset, count); - } + protected final void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); } + } - /** - * Closes the cache entry and makes the socket available for reuse. This - * should be invoked when the end of the body has been reached. - */ - protected final void endOfInput(boolean streamCancelled) throws IOException { - if (cacheRequest != null) { - cacheBody.close(); - } - httpEngine.release(streamCancelled); + protected final void cacheWrite(byte[] buffer, int offset, int count) throws IOException { + if (cacheBody != null) { + cacheBody.write(buffer, offset, count); } + } - /** - * Calls abort on the cache entry and disconnects the socket. This - * should be invoked when the connection is closed unexpectedly to - * invalidate the cache entry and to prevent the HTTP connection from - * being reused. HTTP messages are sent in serial so whenever a message - * cannot be read to completion, subsequent messages cannot be read - * either and the connection must be discarded. - * - *

An earlier implementation skipped the remaining bytes, but this - * requires that the entire transfer be completed. If the intention was - * to cancel the transfer, closing the connection is the only solution. - */ - protected final void unexpectedEndOfInput() { - if (cacheRequest != null) { - cacheRequest.abort(); - } - httpEngine.release(true); + /** + * Closes the cache entry and makes the socket available for reuse. This + * should be invoked when the end of the body has been reached. + */ + protected final void endOfInput(boolean streamCancelled) throws IOException { + if (cacheRequest != null) { + cacheBody.close(); } + httpEngine.release(streamCancelled); + } + + /** + * Calls abort on the cache entry and disconnects the socket. This + * should be invoked when the connection is closed unexpectedly to + * invalidate the cache entry and to prevent the HTTP connection from + * being reused. HTTP messages are sent in serial so whenever a message + * cannot be read to completion, subsequent messages cannot be read + * either and the connection must be discarded. + * + *

An earlier implementation skipped the remaining bytes, but this + * requires that the entire transfer be completed. If the intention was + * to cancel the transfer, closing the connection is the only solution. + */ + protected final void unexpectedEndOfInput() { + if (cacheRequest != null) { + cacheRequest.abort(); + } + httpEngine.release(true); + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java index 5d835ccc6..90675b06b 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java +++ b/src/main/java/com/squareup/okhttp/internal/http/AbstractHttpOutputStream.java @@ -26,15 +26,15 @@ import java.io.OutputStream; * requests to the same server, subclasses should not close the socket stream. */ abstract class AbstractHttpOutputStream extends OutputStream { - protected boolean closed; + protected boolean closed; - @Override public final void write(int data) throws IOException { - write(new byte[] {(byte) data}); - } + @Override public final void write(int data) throws IOException { + write(new byte[] { (byte) data }); + } - protected final void checkNotClosed() throws IOException { - if (closed) { - throw new IOException("stream closed"); - } + protected final void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java b/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java index 0dd096e7a..12e640973 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java @@ -18,97 +18,95 @@ package com.squareup.okhttp.internal.http; final class HeaderParser { - public interface CacheControlHandler { - void handle(String directive, String parameter); + public interface CacheControlHandler { + void handle(String directive, String parameter); + } + + /** Parse a comma-separated list of cache control header values. */ + public static void parseCacheControl(String value, CacheControlHandler handler) { + int pos = 0; + while (pos < value.length()) { + int tokenStart = pos; + pos = skipUntil(value, pos, "=,"); + String directive = value.substring(tokenStart, pos).trim(); + + if (pos == value.length() || value.charAt(pos) == ',') { + pos++; // consume ',' (if necessary) + handler.handle(directive, null); + continue; + } + + pos++; // consume '=' + pos = skipWhitespace(value, pos); + + String parameter; + + // quoted string + if (pos < value.length() && value.charAt(pos) == '\"') { + pos++; // consume '"' open quote + int parameterStart = pos; + pos = skipUntil(value, pos, "\""); + parameter = value.substring(parameterStart, pos); + pos++; // consume '"' close quote (if necessary) + + // unquoted string + } else { + int parameterStart = pos; + pos = skipUntil(value, pos, ","); + parameter = value.substring(parameterStart, pos).trim(); + } + + handler.handle(directive, parameter); } + } - /** - * Parse a comma-separated list of cache control header values. - */ - public static void parseCacheControl(String value, CacheControlHandler handler) { - int pos = 0; - while (pos < value.length()) { - int tokenStart = pos; - pos = skipUntil(value, pos, "=,"); - String directive = value.substring(tokenStart, pos).trim(); - - if (pos == value.length() || value.charAt(pos) == ',') { - pos++; // consume ',' (if necessary) - handler.handle(directive, null); - continue; - } - - pos++; // consume '=' - pos = skipWhitespace(value, pos); - - String parameter; - - // quoted string - if (pos < value.length() && value.charAt(pos) == '\"') { - pos++; // consume '"' open quote - int parameterStart = pos; - pos = skipUntil(value, pos, "\""); - parameter = value.substring(parameterStart, pos); - pos++; // consume '"' close quote (if necessary) - - // unquoted string - } else { - int parameterStart = pos; - pos = skipUntil(value, pos, ","); - parameter = value.substring(parameterStart, pos).trim(); - } - - handler.handle(directive, parameter); - } + /** + * Returns the next index in {@code input} at or after {@code pos} that + * contains a character from {@code characters}. Returns the input length if + * none of the requested characters can be found. + */ + public static int skipUntil(String input, int pos, String characters) { + for (; pos < input.length(); pos++) { + if (characters.indexOf(input.charAt(pos)) != -1) { + break; + } } + return pos; + } - /** - * Returns the next index in {@code input} at or after {@code pos} that - * contains a character from {@code characters}. Returns the input length if - * none of the requested characters can be found. - */ - public static int skipUntil(String input, int pos, String characters) { - for (; pos < input.length(); pos++) { - if (characters.indexOf(input.charAt(pos)) != -1) { - break; - } - } - return pos; + /** + * Returns the next non-whitespace character in {@code input} that is white + * space. Result is undefined if input contains newline characters. + */ + public static int skipWhitespace(String input, int pos) { + for (; pos < input.length(); pos++) { + char c = input.charAt(pos); + if (c != ' ' && c != '\t') { + break; + } } + return pos; + } - /** - * Returns the next non-whitespace character in {@code input} that is white - * space. Result is undefined if input contains newline characters. - */ - public static int skipWhitespace(String input, int pos) { - for (; pos < input.length(); pos++) { - char c = input.charAt(pos); - if (c != ' ' && c != '\t') { - break; - } - } - return pos; + /** + * Returns {@code value} as a positive integer, or 0 if it is negative, or + * -1 if it cannot be parsed. + */ + public static int parseSeconds(String value) { + try { + long seconds = Long.parseLong(value); + if (seconds > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else if (seconds < 0) { + return 0; + } else { + return (int) seconds; + } + } catch (NumberFormatException e) { + return -1; } + } - /** - * Returns {@code value} as a positive integer, or 0 if it is negative, or - * -1 if it cannot be parsed. - */ - public static int parseSeconds(String value) { - try { - long seconds = Long.parseLong(value); - if (seconds > Integer.MAX_VALUE) { - return Integer.MAX_VALUE; - } else if (seconds < 0) { - return 0; - } else { - return (int) seconds; - } - } catch (NumberFormatException e) { - return -1; - } - } - - private HeaderParser() { - } + private HeaderParser() { + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java index 70104a5a9..d1447182b 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java @@ -19,8 +19,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.internal.Base64; import java.io.IOException; import java.net.Authenticator; -import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; -import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.PasswordAuthentication; @@ -29,95 +27,90 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; -/** - * Handles HTTP authentication headers from origin and proxy servers. - */ +import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; +import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED; + +/** Handles HTTP authentication headers from origin and proxy servers. */ public final class HttpAuthenticator { - private HttpAuthenticator() { + private HttpAuthenticator() { + } + + /** + * React to a failed authorization response by looking up new credentials. + * + * @return true if credentials have been added to successorRequestHeaders + * and another request should be attempted. + */ + public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders, + RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException { + if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) { + throw new IllegalArgumentException(); } - /** - * React to a failed authorization response by looking up new credentials. - * - * @return true if credentials have been added to successorRequestHeaders - * and another request should be attempted. - */ - public static boolean processAuthHeader(int responseCode, RawHeaders responseHeaders, - RawHeaders successorRequestHeaders, Proxy proxy, URL url) throws IOException { - if (responseCode != HTTP_PROXY_AUTH && responseCode != HTTP_UNAUTHORIZED) { - throw new IllegalArgumentException(); - } - - // Keep asking for username/password until authorized. - String challengeHeader = responseCode == HTTP_PROXY_AUTH - ? "Proxy-Authenticate" - : "WWW-Authenticate"; - String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url); - if (credentials == null) { - return false; // Could not find credentials so end the request cycle. - } - - // Add authorization credentials, bypassing the already-connected check. - String fieldName = responseCode == HTTP_PROXY_AUTH - ? "Proxy-Authorization" - : "Authorization"; - successorRequestHeaders.set(fieldName, credentials); - return true; + // Keep asking for username/password until authorized. + String challengeHeader = + responseCode == HTTP_PROXY_AUTH ? "Proxy-Authenticate" : "WWW-Authenticate"; + String credentials = getCredentials(responseHeaders, challengeHeader, proxy, url); + if (credentials == null) { + return false; // Could not find credentials so end the request cycle. } - /** - * Returns the authorization credentials that may satisfy the challenge. - * Returns null if a challenge header was not provided or if credentials - * were not available. - */ - private static String getCredentials(RawHeaders responseHeaders, - String challengeHeader, Proxy proxy, URL url) throws IOException { - List challenges = parseChallenges(responseHeaders, challengeHeader); - if (challenges.isEmpty()) { - return null; - } + // Add authorization credentials, bypassing the already-connected check. + String fieldName = responseCode == HTTP_PROXY_AUTH ? "Proxy-Authorization" : "Authorization"; + successorRequestHeaders.set(fieldName, credentials); + return true; + } - for (Challenge challenge : challenges) { - // Use the global authenticator to get the password. - PasswordAuthentication auth; - if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) { - InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); - auth = Authenticator.requestPasswordAuthentication( - proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), - proxyAddress.getPort(), url.getProtocol(), challenge.realm, - challenge.scheme, url, Authenticator.RequestorType.PROXY); - } else { - auth = Authenticator.requestPasswordAuthentication( - url.getHost(), getConnectToInetAddress(proxy, url), url.getPort(), - url.getProtocol(), challenge.realm, challenge.scheme, url, - Authenticator.RequestorType.SERVER); - } - if (auth == null) { - continue; - } - - // Use base64 to encode the username and password. - String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword()); - byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1"); - String encoded = Base64.encode(bytes); - return challenge.scheme + " " + encoded; - } - - return null; + /** + * Returns the authorization credentials that may satisfy the challenge. + * Returns null if a challenge header was not provided or if credentials + * were not available. + */ + private static String getCredentials(RawHeaders responseHeaders, String challengeHeader, + Proxy proxy, URL url) throws IOException { + List challenges = parseChallenges(responseHeaders, challengeHeader); + if (challenges.isEmpty()) { + return null; } - private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException { - return (proxy != null && proxy.type() != Proxy.Type.DIRECT) - ? ((InetSocketAddress) proxy.address()).getAddress() - : InetAddress.getByName(url.getHost()); + for (Challenge challenge : challenges) { + // Use the global authenticator to get the password. + PasswordAuthentication auth; + if (responseHeaders.getResponseCode() == HTTP_PROXY_AUTH) { + InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address(); + auth = Authenticator.requestPasswordAuthentication(proxyAddress.getHostName(), + getConnectToInetAddress(proxy, url), proxyAddress.getPort(), url.getProtocol(), + challenge.realm, challenge.scheme, url, Authenticator.RequestorType.PROXY); + } else { + auth = Authenticator.requestPasswordAuthentication(url.getHost(), + getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(), challenge.realm, + challenge.scheme, url, Authenticator.RequestorType.SERVER); + } + if (auth == null) { + continue; + } + + // Use base64 to encode the username and password. + String usernameAndPassword = auth.getUserName() + ":" + new String(auth.getPassword()); + byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1"); + String encoded = Base64.encode(bytes); + return challenge.scheme + " " + encoded; } - /** - * Parse RFC 2617 challenges. This API is only interested in the scheme - * name and realm. - */ - private static List parseChallenges( - RawHeaders responseHeaders, String challengeHeader) { + return null; + } + + private static InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException { + return (proxy != null && proxy.type() != Proxy.Type.DIRECT) + ? ((InetSocketAddress) proxy.address()).getAddress() : InetAddress.getByName(url.getHost()); + } + + /** + * Parse RFC 2617 challenges. This API is only interested in the scheme + * name and realm. + */ + private static List parseChallenges(RawHeaders responseHeaders, + String challengeHeader) { /* * auth-scheme = token * auth-param = token "=" ( token | quoted-string ) @@ -125,62 +118,60 @@ public final class HttpAuthenticator { * realm = "realm" "=" realm-value * realm-value = quoted-string */ - List result = new ArrayList(); - for (int h = 0; h < responseHeaders.length(); h++) { - if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) { - continue; - } - String value = responseHeaders.getValue(h); - int pos = 0; - while (pos < value.length()) { - int tokenStart = pos; - pos = HeaderParser.skipUntil(value, pos, " "); + List result = new ArrayList(); + for (int h = 0; h < responseHeaders.length(); h++) { + if (!challengeHeader.equalsIgnoreCase(responseHeaders.getFieldName(h))) { + continue; + } + String value = responseHeaders.getValue(h); + int pos = 0; + while (pos < value.length()) { + int tokenStart = pos; + pos = HeaderParser.skipUntil(value, pos, " "); - String scheme = value.substring(tokenStart, pos).trim(); - pos = HeaderParser.skipWhitespace(value, pos); + String scheme = value.substring(tokenStart, pos).trim(); + pos = HeaderParser.skipWhitespace(value, pos); - // TODO: This currently only handles schemes with a 'realm' parameter; - // It needs to be fixed to handle any scheme and any parameters - // http://code.google.com/p/android/issues/detail?id=11140 + // TODO: This currently only handles schemes with a 'realm' parameter; + // It needs to be fixed to handle any scheme and any parameters + // http://code.google.com/p/android/issues/detail?id=11140 - if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) { - break; // Unexpected challenge parameter; give up! - } - - pos += "realm=\"".length(); - int realmStart = pos; - pos = HeaderParser.skipUntil(value, pos, "\""); - String realm = value.substring(realmStart, pos); - pos++; // Consume '"' close quote. - pos = HeaderParser.skipUntil(value, pos, ","); - pos++; // Consume ',' comma. - pos = HeaderParser.skipWhitespace(value, pos); - result.add(new Challenge(scheme, realm)); - } + if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) { + break; // Unexpected challenge parameter; give up! } - return result; + + pos += "realm=\"".length(); + int realmStart = pos; + pos = HeaderParser.skipUntil(value, pos, "\""); + String realm = value.substring(realmStart, pos); + pos++; // Consume '"' close quote. + pos = HeaderParser.skipUntil(value, pos, ","); + pos++; // Consume ',' comma. + pos = HeaderParser.skipWhitespace(value, pos); + result.add(new Challenge(scheme, realm)); + } + } + return result; + } + + /** An RFC 2617 challenge. */ + private static final class Challenge { + final String scheme; + final String realm; + + Challenge(String scheme, String realm) { + this.scheme = scheme; + this.realm = realm; } - /** - * An RFC 2617 challenge. - */ - private static final class Challenge { - final String scheme; - final String realm; - - Challenge(String scheme, String realm) { - this.scheme = scheme; - this.realm = realm; - } - - @Override public boolean equals(Object o) { - return o instanceof Challenge - && ((Challenge) o).scheme.equals(scheme) - && ((Challenge) o).realm.equals(realm); - } - - @Override public int hashCode() { - return scheme.hashCode() + 31 * realm.hashCode(); - } + @Override public boolean equals(Object o) { + return o instanceof Challenge + && ((Challenge) o).scheme.equals(scheme) + && ((Challenge) o).realm.equals(realm); } + + @Override public int hashCode() { + return scheme.hashCode() + 31 * realm.hashCode(); + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java b/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java index 41f03fafc..acb5fda9b 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpDate.java @@ -28,67 +28,55 @@ import java.util.TimeZone; */ final class HttpDate { - /** - * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such - * cookies are on the fast path. - */ - private static final ThreadLocal STANDARD_DATE_FORMAT - = new ThreadLocal() { + /** + * Most websites serve cookies in the blessed format. Eagerly create the parser to ensure such + * cookies are on the fast path. + */ + private static final ThreadLocal STANDARD_DATE_FORMAT = + new ThreadLocal() { @Override protected DateFormat initialValue() { - DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); - rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); - return rfc1123; + DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); + return rfc1123; } - }; + }; - /** - * If we fail to parse a date in a non-standard format, try each of these formats in sequence. - */ - private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] { + /** If we fail to parse a date in a non-standard format, try each of these formats in sequence. */ + private static final String[] BROWSER_COMPATIBLE_DATE_FORMATS = new String[] { /* This list comes from {@code org.apache.http.impl.cookie.BrowserCompatSpec}. */ - "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036 - "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime() - "EEE, dd-MMM-yyyy HH:mm:ss z", - "EEE, dd-MMM-yyyy HH-mm-ss z", - "EEE, dd MMM yy HH:mm:ss z", - "EEE dd-MMM-yyyy HH:mm:ss z", - "EEE dd MMM yyyy HH:mm:ss z", - "EEE dd-MMM-yyyy HH-mm-ss z", - "EEE dd-MMM-yy HH:mm:ss z", - "EEE dd MMM yy HH:mm:ss z", - "EEE,dd-MMM-yy HH:mm:ss z", - "EEE,dd-MMM-yyyy HH:mm:ss z", - "EEE, dd-MM-yyyy HH:mm:ss z", + "EEEE, dd-MMM-yy HH:mm:ss zzz", // RFC 1036 + "EEE MMM d HH:mm:ss yyyy", // ANSI C asctime() + "EEE, dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MMM-yyyy HH-mm-ss z", "EEE, dd MMM yy HH:mm:ss z", + "EEE dd-MMM-yyyy HH:mm:ss z", "EEE dd MMM yyyy HH:mm:ss z", "EEE dd-MMM-yyyy HH-mm-ss z", + "EEE dd-MMM-yy HH:mm:ss z", "EEE dd MMM yy HH:mm:ss z", "EEE,dd-MMM-yy HH:mm:ss z", + "EEE,dd-MMM-yyyy HH:mm:ss z", "EEE, dd-MM-yyyy HH:mm:ss z", /* RI bug 6641315 claims a cookie of this format was once served by www.yahoo.com */ - "EEE MMM d yyyy HH:mm:ss z", - }; + "EEE MMM d yyyy HH:mm:ss z", }; - /** - * Returns the date for {@code value}. Returns null if the value couldn't be - * parsed. - */ - public static Date parse(String value) { - try { - return STANDARD_DATE_FORMAT.get().parse(value); - } catch (ParseException ignore) { - } - for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) { - try { - return new SimpleDateFormat(formatString, Locale.US).parse(value); - } catch (ParseException ignore) { - } - } - return null; + /** + * Returns the date for {@code value}. Returns null if the value couldn't be + * parsed. + */ + public static Date parse(String value) { + try { + return STANDARD_DATE_FORMAT.get().parse(value); + } catch (ParseException ignore) { } + for (String formatString : BROWSER_COMPATIBLE_DATE_FORMATS) { + try { + return new SimpleDateFormat(formatString, Locale.US).parse(value); + } catch (ParseException ignore) { + } + } + return null; + } - /** - * Returns the string for {@code value}. - */ - public static String format(Date value) { - return STANDARD_DATE_FORMAT.get().format(value); - } + /** Returns the string for {@code value}. */ + public static String format(Date value) { + return STANDARD_DATE_FORMAT.get().format(value); + } - private HttpDate() { - } + private HttpDate() { + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java index 1a23a2af2..0b1deb66b 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java @@ -25,9 +25,6 @@ import com.squareup.okhttp.TunnelRequest; import com.squareup.okhttp.internal.Dns; import com.squareup.okhttp.internal.Platform; import com.squareup.okhttp.internal.Util; -import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY; -import static com.squareup.okhttp.internal.Util.getDefaultPort; -import static com.squareup.okhttp.internal.Util.getEffectivePort; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; @@ -50,19 +47,23 @@ import java.util.zip.GZIPInputStream; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; +import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY; +import static com.squareup.okhttp.internal.Util.getDefaultPort; +import static com.squareup.okhttp.internal.Util.getEffectivePort; + /** * Handles a single HTTP request/response pair. Each HTTP engine follows this * lifecycle: *

    - *
  1. It is created. - *
  2. The HTTP request message is sent with sendRequest(). Once the request - * is sent it is an error to modify the request headers. After - * sendRequest() has been called the request body can be written to if - * it exists. - *
  3. The HTTP response message is read with readResponse(). After the - * response has been read the response headers and body can be read. - * All responses have a response body input stream, though in some - * instances this stream is empty. + *
  4. It is created. + *
  5. The HTTP request message is sent with sendRequest(). Once the request + * is sent it is an error to modify the request headers. After + * sendRequest() has been called the request body can be written to if + * it exists. + *
  6. The HTTP response message is read with readResponse(). After the + * response has been read the response headers and body can be read. + * All responses have a response body input stream, though in some + * instances this stream is empty. *
* *

The request and response may be served by the HTTP response cache, by the @@ -74,113 +75,113 @@ import javax.net.ssl.SSLSocketFactory; * required, use {@link #automaticallyReleaseConnectionToPool()}. */ public class HttpEngine { - private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() { - @Override public Map> getHeaders() throws IOException { - Map> result = new HashMap>(); - result.put(null, Collections.singletonList("HTTP/1.1 504 Gateway Timeout")); - return result; - } - @Override public InputStream getBody() throws IOException { - return new ByteArrayInputStream(EMPTY_BYTE_ARRAY); - } - }; - public static final int HTTP_CONTINUE = 100; + private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() { + @Override public Map> getHeaders() throws IOException { + Map> result = new HashMap>(); + result.put(null, Collections.singletonList("HTTP/1.1 504 Gateway Timeout")); + return result; + } + @Override public InputStream getBody() throws IOException { + return new ByteArrayInputStream(EMPTY_BYTE_ARRAY); + } + }; + public static final int HTTP_CONTINUE = 100; - protected final HttpURLConnectionImpl policy; + protected final HttpURLConnectionImpl policy; - protected final String method; + protected final String method; - private ResponseSource responseSource; + private ResponseSource responseSource; - protected Connection connection; - protected RouteSelector routeSelector; - private OutputStream requestBodyOut; + protected Connection connection; + protected RouteSelector routeSelector; + private OutputStream requestBodyOut; - private Transport transport; + private Transport transport; - private InputStream responseTransferIn; - private InputStream responseBodyIn; + private InputStream responseTransferIn; + private InputStream responseBodyIn; - private CacheResponse cacheResponse; - private CacheRequest cacheRequest; + private CacheResponse cacheResponse; + private CacheRequest cacheRequest; - /** The time when the request headers were written, or -1 if they haven't been written yet. */ - long sentRequestMillis = -1; + /** The time when the request headers were written, or -1 if they haven't been written yet. */ + long sentRequestMillis = -1; - /** - * True if this client added an "Accept-Encoding: gzip" header field and is - * therefore responsible for also decompressing the transfer stream. - */ - private boolean transparentGzip; + /** + * True if this client added an "Accept-Encoding: gzip" header field and is + * therefore responsible for also decompressing the transfer stream. + */ + private boolean transparentGzip; - final URI uri; + final URI uri; - final RequestHeaders requestHeaders; + final RequestHeaders requestHeaders; - /** Null until a response is received from the network or the cache. */ - ResponseHeaders responseHeaders; + /** Null until a response is received from the network or the cache. */ + ResponseHeaders responseHeaders; - /* - * The cache response currently being validated on a conditional get. Null - * if the cached response doesn't exist or doesn't need validation. If the - * conditional get succeeds, these will be used for the response headers and - * body. If it fails, these be closed and set to null. - */ - private ResponseHeaders cachedResponseHeaders; - private InputStream cachedResponseBody; + /* + * The cache response currently being validated on a conditional get. Null + * if the cached response doesn't exist or doesn't need validation. If the + * conditional get succeeds, these will be used for the response headers and + * body. If it fails, these be closed and set to null. + */ + private ResponseHeaders cachedResponseHeaders; + private InputStream cachedResponseBody; - /** - * True if the socket connection should be released to the connection pool - * when the response has been fully read. - */ - private boolean automaticallyReleaseConnectionToPool; + /** + * True if the socket connection should be released to the connection pool + * when the response has been fully read. + */ + private boolean automaticallyReleaseConnectionToPool; - /** True if the socket connection is no longer needed by this engine. */ - private boolean connectionReleased; + /** True if the socket connection is no longer needed by this engine. */ + private boolean connectionReleased; - /** - * @param requestHeaders the client's supplied request headers. This class - * creates a private copy that it can mutate. - * @param connection the connection used for an intermediate response - * immediately prior to this request/response pair, such as a same-host - * redirect. This engine assumes ownership of the connection and must - * release it when it is unneeded. - */ - public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, - Connection connection, RetryableOutputStream requestBodyOut) throws IOException { - this.policy = policy; - this.method = method; - this.connection = connection; - this.requestBodyOut = requestBodyOut; + /** + * @param requestHeaders the client's supplied request headers. This class + * creates a private copy that it can mutate. + * @param connection the connection used for an intermediate response + * immediately prior to this request/response pair, such as a same-host + * redirect. This engine assumes ownership of the connection and must + * release it when it is unneeded. + */ + public HttpEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, + Connection connection, RetryableOutputStream requestBodyOut) throws IOException { + this.policy = policy; + this.method = method; + this.connection = connection; + this.requestBodyOut = requestBodyOut; - try { - uri = Platform.get().toUriLenient(policy.getURL()); - } catch (URISyntaxException e) { - throw new IOException(e); - } - - this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders)); + try { + uri = Platform.get().toUriLenient(policy.getURL()); + } catch (URISyntaxException e) { + throw new IOException(e); } - public URI getUri() { - return uri; + this.requestHeaders = new RequestHeaders(uri, new RawHeaders(requestHeaders)); + } + + public URI getUri() { + return uri; + } + + /** + * Figures out what the response source will be, and opens a socket to that + * source if necessary. Prepares the request headers and gets ready to start + * writing the request body if it exists. + */ + public final void sendRequest() throws IOException { + if (responseSource != null) { + return; } - /** - * Figures out what the response source will be, and opens a socket to that - * source if necessary. Prepares the request headers and gets ready to start - * writing the request body if it exists. - */ - public final void sendRequest() throws IOException { - if (responseSource != null) { - return; - } - - prepareRawRequestHeaders(); - initResponseSource(); - if (policy.responseCache instanceof OkResponseCache) { - ((OkResponseCache) policy.responseCache).trackResponse(responseSource); - } + prepareRawRequestHeaders(); + initResponseSource(); + if (policy.responseCache instanceof OkResponseCache) { + ((OkResponseCache) policy.responseCache).trackResponse(responseSource); + } /* * The raw response source may require the network, but the request @@ -188,501 +189,494 @@ public class HttpEngine { * response and use a GATEWAY_TIMEOUT response instead, as specified * by http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4. */ - if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) { - if (responseSource == ResponseSource.CONDITIONAL_CACHE) { - Util.closeQuietly(cachedResponseBody); - } - this.responseSource = ResponseSource.CACHE; - this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE; - RawHeaders rawResponseHeaders - = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true); - setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody()); - } - - if (responseSource.requiresConnection()) { - sendSocketRequest(); - } else if (connection != null) { - policy.connectionPool.recycle(connection); - connection = null; - } + if (requestHeaders.isOnlyIfCached() && responseSource.requiresConnection()) { + if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + Util.closeQuietly(cachedResponseBody); + } + this.responseSource = ResponseSource.CACHE; + this.cacheResponse = GATEWAY_TIMEOUT_RESPONSE; + RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(cacheResponse.getHeaders(), true); + setResponse(new ResponseHeaders(uri, rawResponseHeaders), cacheResponse.getBody()); } - /** - * Initialize the source for this response. It may be corrected later if the - * request headers forbids network use. - */ - private void initResponseSource() throws IOException { - responseSource = ResponseSource.NETWORK; - if (!policy.getUseCaches() || policy.responseCache == null) { - return; - } + if (responseSource.requiresConnection()) { + sendSocketRequest(); + } else if (connection != null) { + policy.connectionPool.recycle(connection); + connection = null; + } + } - CacheResponse candidate = policy.responseCache.get(uri, method, - requestHeaders.getHeaders().toMultimap(false)); - if (candidate == null) { - return; - } - - Map> responseHeadersMap = candidate.getHeaders(); - cachedResponseBody = candidate.getBody(); - if (!acceptCacheResponseType(candidate) - || responseHeadersMap == null - || cachedResponseBody == null) { - Util.closeQuietly(cachedResponseBody); - return; - } - - RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true); - cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders); - long now = System.currentTimeMillis(); - this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); - if (responseSource == ResponseSource.CACHE) { - this.cacheResponse = candidate; - setResponse(cachedResponseHeaders, cachedResponseBody); - } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { - this.cacheResponse = candidate; - } else if (responseSource == ResponseSource.NETWORK) { - Util.closeQuietly(cachedResponseBody); - } else { - throw new AssertionError(); - } + /** + * Initialize the source for this response. It may be corrected later if the + * request headers forbids network use. + */ + private void initResponseSource() throws IOException { + responseSource = ResponseSource.NETWORK; + if (!policy.getUseCaches() || policy.responseCache == null) { + return; } - private void sendSocketRequest() throws IOException { - if (connection == null) { - connect(); - } - - if (transport != null) { - throw new IllegalStateException(); - } - - transport = (Transport) connection.newTransport(this); - - if (hasRequestBody() && requestBodyOut == null) { - // Create a request body if we don't have one already. We'll already - // have one if we're retrying a failed POST. - requestBodyOut = transport.createRequestBody(); - } + CacheResponse candidate = + policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false)); + if (candidate == null) { + return; } - /** - * Connect to the origin server either directly or via a proxy. - */ - protected final void connect() throws IOException { - if (connection != null) { - return; - } - if (routeSelector == null) { - String uriHost = uri.getHost(); - if (uriHost == null) { - throw new UnknownHostException(uri.toString()); - } - Address address = new Address(uriHost, getEffectivePort(uri), - getSslSocketFactory(), getHostnameVerifier(), policy.getProxy()); - routeSelector = new RouteSelector( - address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT); - } - connection = routeSelector.next(); - if (!connection.isConnected()) { - connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), - getTunnelConfig()); - policy.connectionPool.maybeShare(connection); - } - connected(connection); - Proxy proxy = connection.getProxy(); - if (proxy != null) { - policy.setProxy(proxy); - // Add the authority to the request line when we're using a proxy. - requestHeaders.getHeaders().setRequestLine(getRequestLine()); - } + Map> responseHeadersMap = candidate.getHeaders(); + cachedResponseBody = candidate.getBody(); + if (!acceptCacheResponseType(candidate) + || responseHeadersMap == null + || cachedResponseBody == null) { + Util.closeQuietly(cachedResponseBody); + return; } - /** - * Called after a socket connection has been created or retrieved from the - * pool. Subclasses use this hook to get a reference to the TLS data. - */ - protected void connected(Connection connection) { + RawHeaders rawResponseHeaders = RawHeaders.fromMultimap(responseHeadersMap, true); + cachedResponseHeaders = new ResponseHeaders(uri, rawResponseHeaders); + long now = System.currentTimeMillis(); + this.responseSource = cachedResponseHeaders.chooseResponseSource(now, requestHeaders); + if (responseSource == ResponseSource.CACHE) { + this.cacheResponse = candidate; + setResponse(cachedResponseHeaders, cachedResponseBody); + } else if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + this.cacheResponse = candidate; + } else if (responseSource == ResponseSource.NETWORK) { + Util.closeQuietly(cachedResponseBody); + } else { + throw new AssertionError(); + } + } + + private void sendSocketRequest() throws IOException { + if (connection == null) { + connect(); } - /** - * Called immediately before the transport transmits HTTP request headers. - * This is used to observe the sent time should the request be cached. - */ - public void writingRequestHeaders() { - if (sentRequestMillis != -1) { - throw new IllegalStateException(); - } - sentRequestMillis = System.currentTimeMillis(); + if (transport != null) { + throw new IllegalStateException(); } - /** - * @param body the response body, or null if it doesn't exist or isn't - * available. - */ - private void setResponse(ResponseHeaders headers, InputStream body) throws IOException { - if (this.responseBodyIn != null) { - throw new IllegalStateException(); - } - this.responseHeaders = headers; - if (body != null) { - initContentStream(body); - } + transport = (Transport) connection.newTransport(this); + + if (hasRequestBody() && requestBodyOut == null) { + // Create a request body if we don't have one already. We'll already + // have one if we're retrying a failed POST. + requestBodyOut = transport.createRequestBody(); + } + } + + /** Connect to the origin server either directly or via a proxy. */ + protected final void connect() throws IOException { + if (connection != null) { + return; + } + if (routeSelector == null) { + String uriHost = uri.getHost(); + if (uriHost == null) { + throw new UnknownHostException(uri.toString()); + } + Address address = + new Address(uriHost, getEffectivePort(uri), getSslSocketFactory(), getHostnameVerifier(), + policy.getProxy()); + routeSelector = + new RouteSelector(address, uri, policy.proxySelector, policy.connectionPool, Dns.DEFAULT); + } + connection = routeSelector.next(); + if (!connection.isConnected()) { + connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(), getTunnelConfig()); + policy.connectionPool.maybeShare(connection); + } + connected(connection); + Proxy proxy = connection.getProxy(); + if (proxy != null) { + policy.setProxy(proxy); + // Add the authority to the request line when we're using a proxy. + requestHeaders.getHeaders().setRequestLine(getRequestLine()); + } + } + + /** + * Called after a socket connection has been created or retrieved from the + * pool. Subclasses use this hook to get a reference to the TLS data. + */ + protected void connected(Connection connection) { + } + + /** + * Called immediately before the transport transmits HTTP request headers. + * This is used to observe the sent time should the request be cached. + */ + public void writingRequestHeaders() { + if (sentRequestMillis != -1) { + throw new IllegalStateException(); + } + sentRequestMillis = System.currentTimeMillis(); + } + + /** + * @param body the response body, or null if it doesn't exist or isn't + * available. + */ + private void setResponse(ResponseHeaders headers, InputStream body) throws IOException { + if (this.responseBodyIn != null) { + throw new IllegalStateException(); + } + this.responseHeaders = headers; + if (body != null) { + initContentStream(body); + } + } + + boolean hasRequestBody() { + return method.equals("POST") || method.equals("PUT"); + } + + /** Returns the request body or null if this request doesn't have a body. */ + public final OutputStream getRequestBody() { + if (responseSource == null) { + throw new IllegalStateException(); + } + return requestBodyOut; + } + + public final boolean hasResponse() { + return responseHeaders != null; + } + + public final RequestHeaders getRequestHeaders() { + return requestHeaders; + } + + public final ResponseHeaders getResponseHeaders() { + if (responseHeaders == null) { + throw new IllegalStateException(); + } + return responseHeaders; + } + + public final int getResponseCode() { + if (responseHeaders == null) { + throw new IllegalStateException(); + } + return responseHeaders.getHeaders().getResponseCode(); + } + + public final InputStream getResponseBody() { + if (responseHeaders == null) { + throw new IllegalStateException(); + } + return responseBodyIn; + } + + public final CacheResponse getCacheResponse() { + return cacheResponse; + } + + public final Connection getConnection() { + return connection; + } + + /** + * Returns true if {@code cacheResponse} is of the right type. This + * condition is necessary but not sufficient for the cached response to + * be used. + */ + protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { + return true; + } + + private void maybeCache() throws IOException { + // Are we caching at all? + if (!policy.getUseCaches() || policy.responseCache == null) { + return; } - boolean hasRequestBody() { - return method.equals("POST") || method.equals("PUT"); + // Should we cache this response for this request? + if (!responseHeaders.isCacheable(requestHeaders)) { + return; } - /** - * Returns the request body or null if this request doesn't have a body. - */ - public final OutputStream getRequestBody() { - if (responseSource == null) { - throw new IllegalStateException(); - } - return requestBodyOut; + // Offer this request to the cache. + cacheRequest = policy.responseCache.put(uri, getHttpConnectionToCache()); + } + + protected HttpURLConnection getHttpConnectionToCache() { + return policy; + } + + /** + * Cause the socket connection to be released to the connection pool when + * it is no longer needed. If it is already unneeded, it will be pooled + * immediately. Otherwise the connection is held so that redirects can be + * handled by the same connection. + */ + public final void automaticallyReleaseConnectionToPool() { + automaticallyReleaseConnectionToPool = true; + if (connection != null && connectionReleased) { + policy.connectionPool.recycle(connection); + connection = null; + } + } + + /** + * Releases this engine so that its resources may be either reused or + * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless + * the connection will be used to follow a redirect. + */ + public final void release(boolean streamCancelled) { + // If the response body comes from the cache, close it. + if (responseBodyIn == cachedResponseBody) { + Util.closeQuietly(responseBodyIn); } - public final boolean hasResponse() { - return responseHeaders != null; + if (!connectionReleased && connection != null) { + connectionReleased = true; + + if (transport == null || !transport.makeReusable(streamCancelled, requestBodyOut, + responseTransferIn)) { + Util.closeQuietly(connection); + connection = null; + } else if (automaticallyReleaseConnectionToPool) { + policy.connectionPool.recycle(connection); + connection = null; + } } + } - public final RequestHeaders getRequestHeaders() { - return requestHeaders; - } - - public final ResponseHeaders getResponseHeaders() { - if (responseHeaders == null) { - throw new IllegalStateException(); - } - return responseHeaders; - } - - public final int getResponseCode() { - if (responseHeaders == null) { - throw new IllegalStateException(); - } - return responseHeaders.getHeaders().getResponseCode(); - } - - public final InputStream getResponseBody() { - if (responseHeaders == null) { - throw new IllegalStateException(); - } - return responseBodyIn; - } - - public final CacheResponse getCacheResponse() { - return cacheResponse; - } - - public final Connection getConnection() { - return connection; - } - - /** - * Returns true if {@code cacheResponse} is of the right type. This - * condition is necessary but not sufficient for the cached response to - * be used. - */ - protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { - return true; - } - - private void maybeCache() throws IOException { - // Are we caching at all? - if (!policy.getUseCaches() || policy.responseCache == null) { - return; - } - - // Should we cache this response for this request? - if (!responseHeaders.isCacheable(requestHeaders)) { - return; - } - - // Offer this request to the cache. - cacheRequest = policy.responseCache.put(uri, getHttpConnectionToCache()); - } - - protected HttpURLConnection getHttpConnectionToCache() { - return policy; - } - - /** - * Cause the socket connection to be released to the connection pool when - * it is no longer needed. If it is already unneeded, it will be pooled - * immediately. Otherwise the connection is held so that redirects can be - * handled by the same connection. - */ - public final void automaticallyReleaseConnectionToPool() { - automaticallyReleaseConnectionToPool = true; - if (connection != null && connectionReleased) { - policy.connectionPool.recycle(connection); - connection = null; - } - } - - /** - * Releases this engine so that its resources may be either reused or - * closed. Also call {@link #automaticallyReleaseConnectionToPool} unless - * the connection will be used to follow a redirect. - */ - public final void release(boolean streamCancelled) { - // If the response body comes from the cache, close it. - if (responseBodyIn == cachedResponseBody) { - Util.closeQuietly(responseBodyIn); - } - - if (!connectionReleased && connection != null) { - connectionReleased = true; - - if (transport == null || !transport.makeReusable( - streamCancelled, requestBodyOut, responseTransferIn)) { - Util.closeQuietly(connection); - connection = null; - } else if (automaticallyReleaseConnectionToPool) { - policy.connectionPool.recycle(connection); - connection = null; - } - } - } - - private void initContentStream(InputStream transferStream) throws IOException { - responseTransferIn = transferStream; - if (transparentGzip && responseHeaders.isContentEncodingGzip()) { + private void initContentStream(InputStream transferStream) throws IOException { + responseTransferIn = transferStream; + if (transparentGzip && responseHeaders.isContentEncodingGzip()) { /* * If the response was transparently gzipped, remove the gzip header field * so clients don't double decompress. http://b/3009828 */ - responseHeaders.stripContentEncoding(); - responseBodyIn = new GZIPInputStream(transferStream); - } else { - responseBodyIn = transferStream; - } + responseHeaders.stripContentEncoding(); + responseBodyIn = new GZIPInputStream(transferStream); + } else { + responseBodyIn = transferStream; + } + } + + /** + * Returns true if the response must have a (possibly 0-length) body. + * See RFC 2616 section 4.3. + */ + public final boolean hasResponseBody() { + int responseCode = responseHeaders.getHeaders().getResponseCode(); + + // HEAD requests never yield a body regardless of the response headers. + if (method.equals("HEAD")) { + return false; } - /** - * Returns true if the response must have a (possibly 0-length) body. - * See RFC 2616 section 4.3. - */ - public final boolean hasResponseBody() { - int responseCode = responseHeaders.getHeaders().getResponseCode(); - - // HEAD requests never yield a body regardless of the response headers. - if (method.equals("HEAD")) { - return false; - } - - if ((responseCode < HTTP_CONTINUE || responseCode >= 200) - && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT - && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) { - return true; - } + if ((responseCode < HTTP_CONTINUE || responseCode >= 200) + && responseCode != HttpURLConnectionImpl.HTTP_NO_CONTENT + && responseCode != HttpURLConnectionImpl.HTTP_NOT_MODIFIED) { + return true; + } /* * If the Content-Length or Transfer-Encoding headers disagree with the * response code, the response is malformed. For best compatibility, we * honor the headers. */ - if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) { - return true; - } - - return false; + if (responseHeaders.getContentLength() != -1 || responseHeaders.isChunked()) { + return true; } - /** - * Populates requestHeaders with defaults and cookies. - * - *

This client doesn't specify a default {@code Accept} header because it - * doesn't know what content types the application is interested in. - */ - private void prepareRawRequestHeaders() throws IOException { - requestHeaders.getHeaders().setRequestLine(getRequestLine()); + return false; + } - if (requestHeaders.getUserAgent() == null) { - requestHeaders.setUserAgent(getDefaultUserAgent()); - } + /** + * Populates requestHeaders with defaults and cookies. + * + *

This client doesn't specify a default {@code Accept} header because it + * doesn't know what content types the application is interested in. + */ + private void prepareRawRequestHeaders() throws IOException { + requestHeaders.getHeaders().setRequestLine(getRequestLine()); - if (requestHeaders.getHost() == null) { - requestHeaders.setHost(getOriginAddress(policy.getURL())); - } - - if ((connection == null || connection.getHttpMinorVersion() != 0) - && requestHeaders.getConnection() == null) { - requestHeaders.setConnection("Keep-Alive"); - } - - if (requestHeaders.getAcceptEncoding() == null) { - transparentGzip = true; - requestHeaders.setAcceptEncoding("gzip"); - } - - if (hasRequestBody() && requestHeaders.getContentType() == null) { - requestHeaders.setContentType("application/x-www-form-urlencoded"); - } - - long ifModifiedSince = policy.getIfModifiedSince(); - if (ifModifiedSince != 0) { - requestHeaders.setIfModifiedSince(new Date(ifModifiedSince)); - } - - CookieHandler cookieHandler = policy.cookieHandler; - if (cookieHandler != null) { - requestHeaders.addCookies( - cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false))); - } + if (requestHeaders.getUserAgent() == null) { + requestHeaders.setUserAgent(getDefaultUserAgent()); } - /** - * Returns the request status line, like "GET / HTTP/1.1". This is exposed - * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so - * it needs to be set even if the transport is SPDY. - */ - String getRequestLine() { - String protocol = (connection == null || connection.getHttpMinorVersion() != 0) - ? "HTTP/1.1" - : "HTTP/1.0"; - return method + " " + requestString() + " " + protocol; + if (requestHeaders.getHost() == null) { + requestHeaders.setHost(getOriginAddress(policy.getURL())); } - private String requestString() { - URL url = policy.getURL(); - if (includeAuthorityInRequestLine()) { - return url.toString(); - } else { - return requestPath(url); - } + if ((connection == null || connection.getHttpMinorVersion() != 0) + && requestHeaders.getConnection() == null) { + requestHeaders.setConnection("Keep-Alive"); } - /** - * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never - * empty, even if the request URL is. Includes the query component if it - * exists. - */ - public static String requestPath(URL url) { - String fileOnly = url.getFile(); - if (fileOnly == null) { - return "/"; - } else if (!fileOnly.startsWith("/")) { - return "/" + fileOnly; - } else { - return fileOnly; - } + if (requestHeaders.getAcceptEncoding() == null) { + transparentGzip = true; + requestHeaders.setAcceptEncoding("gzip"); } - /** - * Returns true if the request line should contain the full URL with host - * and port (like "GET http://android.com/foo HTTP/1.1") or only the path - * (like "GET /foo HTTP/1.1"). - * - *

This is non-final because for HTTPS it's never necessary to supply the - * full URL, even if a proxy is in use. - */ - protected boolean includeAuthorityInRequestLine() { - return policy.usingProxy(); + if (hasRequestBody() && requestHeaders.getContentType() == null) { + requestHeaders.setContentType("application/x-www-form-urlencoded"); } - /** - * Returns the SSL configuration for connections created by this engine. - * We cannot reuse HTTPS connections if the socket factory has changed. - */ - protected SSLSocketFactory getSslSocketFactory() { - return null; + long ifModifiedSince = policy.getIfModifiedSince(); + if (ifModifiedSince != 0) { + requestHeaders.setIfModifiedSince(new Date(ifModifiedSince)); } - /** - * Returns the hostname verifier for connections created by this engine. We - * cannot reuse HTTPS connections if the hostname verifier has changed. - */ - protected HostnameVerifier getHostnameVerifier() { - return null; + CookieHandler cookieHandler = policy.cookieHandler; + if (cookieHandler != null) { + requestHeaders.addCookies( + cookieHandler.get(uri, requestHeaders.getHeaders().toMultimap(false))); + } + } + + /** + * Returns the request status line, like "GET / HTTP/1.1". This is exposed + * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so + * it needs to be set even if the transport is SPDY. + */ + String getRequestLine() { + String protocol = + (connection == null || connection.getHttpMinorVersion() != 0) ? "HTTP/1.1" : "HTTP/1.0"; + return method + " " + requestString() + " " + protocol; + } + + private String requestString() { + URL url = policy.getURL(); + if (includeAuthorityInRequestLine()) { + return url.toString(); + } else { + return requestPath(url); + } + } + + /** + * Returns the path to request, like the '/' in 'GET / HTTP/1.1'. Never + * empty, even if the request URL is. Includes the query component if it + * exists. + */ + public static String requestPath(URL url) { + String fileOnly = url.getFile(); + if (fileOnly == null) { + return "/"; + } else if (!fileOnly.startsWith("/")) { + return "/" + fileOnly; + } else { + return fileOnly; + } + } + + /** + * Returns true if the request line should contain the full URL with host + * and port (like "GET http://android.com/foo HTTP/1.1") or only the path + * (like "GET /foo HTTP/1.1"). + * + *

This is non-final because for HTTPS it's never necessary to supply the + * full URL, even if a proxy is in use. + */ + protected boolean includeAuthorityInRequestLine() { + return policy.usingProxy(); + } + + /** + * Returns the SSL configuration for connections created by this engine. + * We cannot reuse HTTPS connections if the socket factory has changed. + */ + protected SSLSocketFactory getSslSocketFactory() { + return null; + } + + /** + * Returns the hostname verifier for connections created by this engine. We + * cannot reuse HTTPS connections if the hostname verifier has changed. + */ + protected HostnameVerifier getHostnameVerifier() { + return null; + } + + public static String getDefaultUserAgent() { + String agent = System.getProperty("http.agent"); + return agent != null ? agent : ("Java" + System.getProperty("java.version")); + } + + public static String getOriginAddress(URL url) { + int port = url.getPort(); + String result = url.getHost(); + if (port > 0 && port != getDefaultPort(url.getProtocol())) { + result = result + ":" + port; + } + return result; + } + + /** + * Flushes the remaining request header and body, parses the HTTP response + * headers and starts reading the HTTP response body if it exists. + */ + public final void readResponse() throws IOException { + if (hasResponse()) { + return; } - public static String getDefaultUserAgent() { - String agent = System.getProperty("http.agent"); - return agent != null ? agent : ("Java" + System.getProperty("java.version")); + if (responseSource == null) { + throw new IllegalStateException("readResponse() without sendRequest()"); } - public static String getOriginAddress(URL url) { - int port = url.getPort(); - String result = url.getHost(); - if (port > 0 && port != getDefaultPort(url.getProtocol())) { - result = result + ":" + port; - } - return result; + if (!responseSource.requiresConnection()) { + return; } - /** - * Flushes the remaining request header and body, parses the HTTP response - * headers and starts reading the HTTP response body if it exists. - */ - public final void readResponse() throws IOException { - if (hasResponse()) { - return; - } - - if (responseSource == null) { - throw new IllegalStateException("readResponse() without sendRequest()"); - } - - if (!responseSource.requiresConnection()) { - return; - } - - if (sentRequestMillis == -1) { - if (requestBodyOut instanceof RetryableOutputStream) { - int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength(); - requestHeaders.setContentLength(contentLength); - } - transport.writeRequestHeaders(); - } - - if (requestBodyOut != null) { - requestBodyOut.close(); - if (requestBodyOut instanceof RetryableOutputStream) { - transport.writeRequestBody((RetryableOutputStream) requestBodyOut); - } - } - - transport.flushRequest(); - - responseHeaders = transport.readResponseHeaders(); - responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis()); - - if (responseSource == ResponseSource.CONDITIONAL_CACHE) { - if (cachedResponseHeaders.validate(responseHeaders)) { - release(false); - ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); - setResponse(combinedHeaders, cachedResponseBody); - if (policy.responseCache instanceof OkResponseCache) { - OkResponseCache httpResponseCache - = (OkResponseCache) policy.responseCache; - httpResponseCache.trackConditionalCacheHit(); - httpResponseCache.update(cacheResponse, getHttpConnectionToCache()); - } - return; - } else { - Util.closeQuietly(cachedResponseBody); - } - } - - if (hasResponseBody()) { - maybeCache(); // reentrant. this calls into user code which may call back into this! - } - - initContentStream(transport.getTransferStream(cacheRequest)); + if (sentRequestMillis == -1) { + if (requestBodyOut instanceof RetryableOutputStream) { + int contentLength = ((RetryableOutputStream) requestBodyOut).contentLength(); + requestHeaders.setContentLength(contentLength); + } + transport.writeRequestHeaders(); } - protected TunnelRequest getTunnelConfig() { - return null; + if (requestBodyOut != null) { + requestBodyOut.close(); + if (requestBodyOut instanceof RetryableOutputStream) { + transport.writeRequestBody((RetryableOutputStream) requestBodyOut); + } } - public void receiveHeaders(RawHeaders headers) throws IOException { - CookieHandler cookieHandler = policy.cookieHandler; - if (cookieHandler != null) { - cookieHandler.put(uri, headers.toMultimap(true)); + transport.flushRequest(); + + responseHeaders = transport.readResponseHeaders(); + responseHeaders.setLocalTimestamps(sentRequestMillis, System.currentTimeMillis()); + + if (responseSource == ResponseSource.CONDITIONAL_CACHE) { + if (cachedResponseHeaders.validate(responseHeaders)) { + release(false); + ResponseHeaders combinedHeaders = cachedResponseHeaders.combine(responseHeaders); + setResponse(combinedHeaders, cachedResponseBody); + if (policy.responseCache instanceof OkResponseCache) { + OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache; + httpResponseCache.trackConditionalCacheHit(); + httpResponseCache.update(cacheResponse, getHttpConnectionToCache()); } + return; + } else { + Util.closeQuietly(cachedResponseBody); + } } + + if (hasResponseBody()) { + maybeCache(); // reentrant. this calls into user code which may call back into this! + } + + initContentStream(transport.getTransferStream(cacheRequest)); + } + + protected TunnelRequest getTunnelConfig() { + return null; + } + + public void receiveHeaders(RawHeaders headers) throws IOException { + CookieHandler cookieHandler = policy.cookieHandler; + if (cookieHandler != null) { + cookieHandler.put(uri, headers.toMultimap(true)); + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java b/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java index 4bdd8fc27..7beb2ef3e 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpResponseCache.java @@ -18,12 +18,10 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.OkResponseCache; import com.squareup.okhttp.ResponseSource; -import com.squareup.okhttp.internal.Util; -import static com.squareup.okhttp.internal.Util.US_ASCII; -import static com.squareup.okhttp.internal.Util.UTF_8; import com.squareup.okhttp.internal.Base64; import com.squareup.okhttp.internal.DiskLruCache; import com.squareup.okhttp.internal.StrictLineReader; +import com.squareup.okhttp.internal.Util; import java.io.BufferedWriter; import java.io.ByteArrayInputStream; import java.io.File; @@ -56,557 +54,554 @@ import java.util.Map; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLPeerUnverifiedException; +import static com.squareup.okhttp.internal.Util.US_ASCII; +import static com.squareup.okhttp.internal.Util.UTF_8; + /** * Cache responses in a directory on the file system. Most clients should use * {@code android.net.HttpResponseCache}, the stable, documented front end for * this. */ public final class HttpResponseCache extends ResponseCache implements OkResponseCache { - private static final char[] DIGITS = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' - }; + private static final char[] DIGITS = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; - // TODO: add APIs to iterate the cache? - private static final int VERSION = 201105; - private static final int ENTRY_METADATA = 0; - private static final int ENTRY_BODY = 1; - private static final int ENTRY_COUNT = 2; + // TODO: add APIs to iterate the cache? + private static final int VERSION = 201105; + private static final int ENTRY_METADATA = 0; + private static final int ENTRY_BODY = 1; + private static final int ENTRY_COUNT = 2; - private final DiskLruCache cache; + private final DiskLruCache cache; - /* read and write statistics, all guarded by 'this' */ - private int writeSuccessCount; - private int writeAbortCount; - private int networkCount; - private int hitCount; - private int requestCount; + /* read and write statistics, all guarded by 'this' */ + private int writeSuccessCount; + private int writeAbortCount; + private int networkCount; + private int hitCount; + private int requestCount; - public HttpResponseCache(File directory, long maxSize) throws IOException { - cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); + public HttpResponseCache(File directory, long maxSize) throws IOException { + cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize); + } + + private String uriToKey(URI uri) { + try { + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8")); + return bytesToHexString(md5bytes); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } catch (UnsupportedEncodingException e) { + throw new AssertionError(e); + } + } + + private static String bytesToHexString(byte[] bytes) { + char[] digits = DIGITS; + char[] buf = new char[bytes.length * 2]; + int c = 0; + for (byte b : bytes) { + buf[c++] = digits[(b >> 4) & 0xf]; + buf[c++] = digits[b & 0xf]; + } + return new String(buf); + } + + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) { + String key = uriToKey(uri); + DiskLruCache.Snapshot snapshot; + Entry entry; + try { + snapshot = cache.get(key); + if (snapshot == null) { + return null; + } + entry = new Entry(snapshot.getInputStream(ENTRY_METADATA)); + } catch (IOException e) { + // Give up because the cache cannot be read. + return null; } - private String uriToKey(URI uri) { - try { - MessageDigest messageDigest = MessageDigest.getInstance("MD5"); - byte[] md5bytes = messageDigest.digest(uri.toString().getBytes("UTF-8")); - return bytesToHexString(md5bytes); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (UnsupportedEncodingException e) { - throw new AssertionError(e); - } + if (!entry.matches(uri, requestMethod, requestHeaders)) { + snapshot.close(); + return null; } - private static String bytesToHexString(byte[] bytes) { - char[] digits = DIGITS; - char[] buf = new char[bytes.length * 2]; - int c = 0; - for (byte b : bytes) { - buf[c++] = digits[(b >> 4) & 0xf]; - buf[c++] = digits[b & 0xf]; - } - return new String(buf); + return entry.isHttps() ? new EntrySecureCacheResponse(entry, snapshot) + : new EntryCacheResponse(entry, snapshot); + } + + @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { + if (!(urlConnection instanceof HttpURLConnection)) { + return null; } - @Override public CacheResponse get(URI uri, String requestMethod, - Map> requestHeaders) { - String key = uriToKey(uri); - DiskLruCache.Snapshot snapshot; - Entry entry; - try { - snapshot = cache.get(key); - if (snapshot == null) { - return null; - } - entry = new Entry(snapshot.getInputStream(ENTRY_METADATA)); - } catch (IOException e) { - // Give up because the cache cannot be read. - return null; - } + HttpURLConnection httpConnection = (HttpURLConnection) urlConnection; + String requestMethod = httpConnection.getRequestMethod(); + String key = uriToKey(uri); - if (!entry.matches(uri, requestMethod, requestHeaders)) { - snapshot.close(); - return null; - } - - return entry.isHttps() - ? new EntrySecureCacheResponse(entry, snapshot) - : new EntryCacheResponse(entry, snapshot); - } - - @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException { - if (!(urlConnection instanceof HttpURLConnection)) { - return null; - } - - HttpURLConnection httpConnection = (HttpURLConnection) urlConnection; - String requestMethod = httpConnection.getRequestMethod(); - String key = uriToKey(uri); - - if (requestMethod.equals("POST") - || requestMethod.equals("PUT") - || requestMethod.equals("DELETE")) { - try { - cache.remove(key); - } catch (IOException ignored) { - // The cache cannot be written. - } - return null; - } else if (!requestMethod.equals("GET")) { + if (requestMethod.equals("POST") || requestMethod.equals("PUT") || requestMethod.equals( + "DELETE")) { + try { + cache.remove(key); + } catch (IOException ignored) { + // The cache cannot be written. + } + return null; + } else if (!requestMethod.equals("GET")) { /* * Don't cache non-GET responses. We're technically allowed to cache * HEAD requests and some POST requests, but the complexity of doing * so is high and the benefit is low. */ - return null; - } - - HttpEngine httpEngine = getHttpEngine(httpConnection); - if (httpEngine == null) { - // Don't cache unless the HTTP implementation is ours. - return null; - } - - ResponseHeaders response = httpEngine.getResponseHeaders(); - if (response.hasVaryAll()) { - return null; - } - - RawHeaders varyHeaders = httpEngine.getRequestHeaders().getHeaders().getAll( - response.getVaryFields()); - Entry entry = new Entry(uri, varyHeaders, httpConnection); - DiskLruCache.Editor editor = null; - try { - editor = cache.edit(key); - if (editor == null) { - return null; - } - entry.writeTo(editor); - return new CacheRequestImpl(editor); - } catch (IOException e) { - abortQuietly(editor); - return null; - } + return null; } - /** - * Handles a conditional request hit by updating the stored cache response - * with the headers from {@code httpConnection}. The cached response body is - * not updated. If the stored response has changed since {@code - * conditionalCacheHit} was returned, this does nothing. - */ - @Override public void update(CacheResponse conditionalCacheHit, - HttpURLConnection httpConnection) throws IOException { - HttpEngine httpEngine = getHttpEngine(httpConnection); - URI uri = httpEngine.getUri(); - ResponseHeaders response = httpEngine.getResponseHeaders(); - RawHeaders varyHeaders = httpEngine.getRequestHeaders().getHeaders() - .getAll(response.getVaryFields()); - Entry entry = new Entry(uri, varyHeaders, httpConnection); - DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse) - ? ((EntryCacheResponse) conditionalCacheHit).snapshot - : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot; - DiskLruCache.Editor editor = null; - try { - editor = snapshot.edit(); // returns null if snapshot is not current - if (editor != null) { - entry.writeTo(editor); - editor.commit(); - } - } catch (IOException e) { - abortQuietly(editor); - } + HttpEngine httpEngine = getHttpEngine(httpConnection); + if (httpEngine == null) { + // Don't cache unless the HTTP implementation is ours. + return null; } - private void abortQuietly(DiskLruCache.Editor editor) { - // Give up because the cache cannot be written. - try { - if (editor != null) { - editor.abort(); - } - } catch (IOException ignored) { - } + ResponseHeaders response = httpEngine.getResponseHeaders(); + if (response.hasVaryAll()) { + return null; } - private HttpEngine getHttpEngine(URLConnection httpConnection) { - if (httpConnection instanceof HttpURLConnectionImpl) { - return ((HttpURLConnectionImpl) httpConnection).getHttpEngine(); - } else if (httpConnection instanceof HttpsURLConnectionImpl) { - return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine(); - } else { - return null; - } + RawHeaders varyHeaders = + httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields()); + Entry entry = new Entry(uri, varyHeaders, httpConnection); + DiskLruCache.Editor editor = null; + try { + editor = cache.edit(key); + if (editor == null) { + return null; + } + entry.writeTo(editor); + return new CacheRequestImpl(editor); + } catch (IOException e) { + abortQuietly(editor); + return null; } + } - public DiskLruCache getCache() { - return cache; + /** + * Handles a conditional request hit by updating the stored cache response + * with the headers from {@code httpConnection}. The cached response body is + * not updated. If the stored response has changed since {@code + * conditionalCacheHit} was returned, this does nothing. + */ + @Override public void update(CacheResponse conditionalCacheHit, HttpURLConnection httpConnection) + throws IOException { + HttpEngine httpEngine = getHttpEngine(httpConnection); + URI uri = httpEngine.getUri(); + ResponseHeaders response = httpEngine.getResponseHeaders(); + RawHeaders varyHeaders = + httpEngine.getRequestHeaders().getHeaders().getAll(response.getVaryFields()); + Entry entry = new Entry(uri, varyHeaders, httpConnection); + DiskLruCache.Snapshot snapshot = (conditionalCacheHit instanceof EntryCacheResponse) + ? ((EntryCacheResponse) conditionalCacheHit).snapshot + : ((EntrySecureCacheResponse) conditionalCacheHit).snapshot; + DiskLruCache.Editor editor = null; + try { + editor = snapshot.edit(); // returns null if snapshot is not current + if (editor != null) { + entry.writeTo(editor); + editor.commit(); + } + } catch (IOException e) { + abortQuietly(editor); } + } - public synchronized int getWriteAbortCount() { - return writeAbortCount; + private void abortQuietly(DiskLruCache.Editor editor) { + // Give up because the cache cannot be written. + try { + if (editor != null) { + editor.abort(); + } + } catch (IOException ignored) { } + } - public synchronized int getWriteSuccessCount() { - return writeSuccessCount; + private HttpEngine getHttpEngine(URLConnection httpConnection) { + if (httpConnection instanceof HttpURLConnectionImpl) { + return ((HttpURLConnectionImpl) httpConnection).getHttpEngine(); + } else if (httpConnection instanceof HttpsURLConnectionImpl) { + return ((HttpsURLConnectionImpl) httpConnection).getHttpEngine(); + } else { + return null; } + } - public synchronized void trackResponse(ResponseSource source) { - requestCount++; + public DiskLruCache getCache() { + return cache; + } - switch (source) { - case CACHE: - hitCount++; - break; - case CONDITIONAL_CACHE: - case NETWORK: - networkCount++; - break; - } - } + public synchronized int getWriteAbortCount() { + return writeAbortCount; + } - public synchronized void trackConditionalCacheHit() { + public synchronized int getWriteSuccessCount() { + return writeSuccessCount; + } + + public synchronized void trackResponse(ResponseSource source) { + requestCount++; + + switch (source) { + case CACHE: hitCount++; + break; + case CONDITIONAL_CACHE: + case NETWORK: + networkCount++; + break; + } + } + + public synchronized void trackConditionalCacheHit() { + hitCount++; + } + + public synchronized int getNetworkCount() { + return networkCount; + } + + public synchronized int getHitCount() { + return hitCount; + } + + public synchronized int getRequestCount() { + return requestCount; + } + + private final class CacheRequestImpl extends CacheRequest { + private final DiskLruCache.Editor editor; + private OutputStream cacheOut; + private boolean done; + private OutputStream body; + + public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { + this.editor = editor; + this.cacheOut = editor.newOutputStream(ENTRY_BODY); + this.body = new FilterOutputStream(cacheOut) { + @Override public void close() throws IOException { + synchronized (HttpResponseCache.this) { + if (done) { + return; + } + done = true; + writeSuccessCount++; + } + super.close(); + editor.commit(); + } + + @Override + public void write(byte[] buffer, int offset, int length) throws IOException { + // Since we don't override "write(int oneByte)", we can write directly to "out" + // and avoid the inefficient implementation from the FilterOutputStream. + out.write(buffer, offset, length); + } + }; } - public synchronized int getNetworkCount() { - return networkCount; + @Override public void abort() { + synchronized (HttpResponseCache.this) { + if (done) { + return; + } + done = true; + writeAbortCount++; + } + Util.closeQuietly(cacheOut); + try { + editor.abort(); + } catch (IOException ignored) { + } } - public synchronized int getHitCount() { - return hitCount; + @Override public OutputStream getBody() throws IOException { + return body; } + } - public synchronized int getRequestCount() { - return requestCount; - } + private static final class Entry { + private final String uri; + private final RawHeaders varyHeaders; + private final String requestMethod; + private final RawHeaders responseHeaders; + private final String cipherSuite; + private final Certificate[] peerCertificates; + private final Certificate[] localCertificates; - private final class CacheRequestImpl extends CacheRequest { - private final DiskLruCache.Editor editor; - private OutputStream cacheOut; - private boolean done; - private OutputStream body; - - public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException { - this.editor = editor; - this.cacheOut = editor.newOutputStream(ENTRY_BODY); - this.body = new FilterOutputStream(cacheOut) { - @Override public void close() throws IOException { - synchronized (HttpResponseCache.this) { - if (done) { - return; - } - done = true; - writeSuccessCount++; - } - super.close(); - editor.commit(); - } - - @Override - public void write(byte[] buffer, int offset, int length) throws IOException { - // Since we don't override "write(int oneByte)", we can write directly to "out" - // and avoid the inefficient implementation from the FilterOutputStream. - out.write(buffer, offset, length); - } - }; - } - - @Override public void abort() { - synchronized (HttpResponseCache.this) { - if (done) { - return; - } - done = true; - writeAbortCount++; - } - Util.closeQuietly(cacheOut); - try { - editor.abort(); - } catch (IOException ignored) { - } - } - - @Override public OutputStream getBody() throws IOException { - return body; - } - } - - private static final class Entry { - private final String uri; - private final RawHeaders varyHeaders; - private final String requestMethod; - private final RawHeaders responseHeaders; - private final String cipherSuite; - private final Certificate[] peerCertificates; - private final Certificate[] localCertificates; - - /* - * Reads an entry from an input stream. A typical entry looks like this: - * http://google.com/foo - * GET - * 2 - * Accept-Language: fr-CA - * Accept-Charset: UTF-8 - * HTTP/1.1 200 OK - * 3 - * Content-Type: image/png - * Content-Length: 100 - * Cache-Control: max-age=600 - * - * A typical HTTPS file looks like this: - * https://google.com/foo - * GET - * 2 - * Accept-Language: fr-CA - * Accept-Charset: UTF-8 - * HTTP/1.1 200 OK - * 3 - * Content-Type: image/png - * Content-Length: 100 - * Cache-Control: max-age=600 - * - * AES_256_WITH_MD5 - * 2 - * base64-encoded peerCertificate[0] - * base64-encoded peerCertificate[1] - * -1 - * - * The file is newline separated. The first two lines are the URL and - * the request method. Next is the number of HTTP Vary request header - * lines, followed by those lines. - * - * Next is the response status line, followed by the number of HTTP - * response header lines, followed by those lines. - * - * HTTPS responses also contain SSL session information. This begins - * with a blank line, and then a line containing the cipher suite. Next - * is the length of the peer certificate chain. These certificates are - * base64-encoded and appear each on their own line. The next line - * contains the length of the local certificate chain. These - * certificates are also base64-encoded and appear each on their own - * line. A length of -1 is used to encode a null array. - */ - public Entry(InputStream in) throws IOException { - try { - StrictLineReader reader = new StrictLineReader(in, US_ASCII); - uri = reader.readLine(); - requestMethod = reader.readLine(); - varyHeaders = new RawHeaders(); - int varyRequestHeaderLineCount = reader.readInt(); - for (int i = 0; i < varyRequestHeaderLineCount; i++) { - varyHeaders.addLine(reader.readLine()); - } - - responseHeaders = new RawHeaders(); - responseHeaders.setStatusLine(reader.readLine()); - int responseHeaderLineCount = reader.readInt(); - for (int i = 0; i < responseHeaderLineCount; i++) { - responseHeaders.addLine(reader.readLine()); - } - - if (isHttps()) { - String blank = reader.readLine(); - if (!blank.isEmpty()) { - throw new IOException("expected \"\" but was \"" + blank + "\""); - } - cipherSuite = reader.readLine(); - peerCertificates = readCertArray(reader); - localCertificates = readCertArray(reader); - } else { - cipherSuite = null; - peerCertificates = null; - localCertificates = null; - } - } finally { - in.close(); - } - } - - public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection) - throws IOException { - this.uri = uri.toString(); - this.varyHeaders = varyHeaders; - this.requestMethod = httpConnection.getRequestMethod(); - this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true); - - if (isHttps()) { - HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection; - cipherSuite = httpsConnection.getCipherSuite(); - Certificate[] peerCertificatesNonFinal = null; - try { - peerCertificatesNonFinal = httpsConnection.getServerCertificates(); - } catch (SSLPeerUnverifiedException ignored) { - } - peerCertificates = peerCertificatesNonFinal; - localCertificates = httpsConnection.getLocalCertificates(); - } else { - cipherSuite = null; - peerCertificates = null; - localCertificates = null; - } - } - - public void writeTo(DiskLruCache.Editor editor) throws IOException { - OutputStream out = editor.newOutputStream(ENTRY_METADATA); - Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8)); - - writer.write(uri + '\n'); - writer.write(requestMethod + '\n'); - writer.write(Integer.toString(varyHeaders.length()) + '\n'); - for (int i = 0; i < varyHeaders.length(); i++) { - writer.write(varyHeaders.getFieldName(i) + ": " - + varyHeaders.getValue(i) + '\n'); - } - - writer.write(responseHeaders.getStatusLine() + '\n'); - writer.write(Integer.toString(responseHeaders.length()) + '\n'); - for (int i = 0; i < responseHeaders.length(); i++) { - writer.write(responseHeaders.getFieldName(i) + ": " - + responseHeaders.getValue(i) + '\n'); - } - - if (isHttps()) { - writer.write('\n'); - writer.write(cipherSuite + '\n'); - writeCertArray(writer, peerCertificates); - writeCertArray(writer, localCertificates); - } - writer.close(); - } - - private boolean isHttps() { - return uri.startsWith("https://"); - } - - private Certificate[] readCertArray(StrictLineReader reader) throws IOException { - int length = reader.readInt(); - if (length == -1) { - return null; - } - try { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - Certificate[] result = new Certificate[length]; - for (int i = 0; i < result.length; i++) { - String line = reader.readLine(); - byte[] bytes = Base64.decode(line.getBytes("US-ASCII")); - result[i] = certificateFactory.generateCertificate( - new ByteArrayInputStream(bytes)); - } - return result; - } catch (CertificateException e) { - throw new IOException(e); - } - } - - private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException { - if (certificates == null) { - writer.write("-1\n"); - return; - } - try { - writer.write(Integer.toString(certificates.length) + '\n'); - for (Certificate certificate : certificates) { - byte[] bytes = certificate.getEncoded(); - String line = Base64.encode(bytes); - writer.write(line + '\n'); - } - } catch (CertificateEncodingException e) { - throw new IOException(e); - } - } - - public boolean matches(URI uri, String requestMethod, - Map> requestHeaders) { - return this.uri.equals(uri.toString()) - && this.requestMethod.equals(requestMethod) - && new ResponseHeaders(uri, responseHeaders) - .varyMatches(varyHeaders.toMultimap(false), requestHeaders); - } - } - - /** - * Returns an input stream that reads the body of a snapshot, closing the - * snapshot when the stream is closed. + /* + * Reads an entry from an input stream. A typical entry looks like this: + * http://google.com/foo + * GET + * 2 + * Accept-Language: fr-CA + * Accept-Charset: UTF-8 + * HTTP/1.1 200 OK + * 3 + * Content-Type: image/png + * Content-Length: 100 + * Cache-Control: max-age=600 + * + * A typical HTTPS file looks like this: + * https://google.com/foo + * GET + * 2 + * Accept-Language: fr-CA + * Accept-Charset: UTF-8 + * HTTP/1.1 200 OK + * 3 + * Content-Type: image/png + * Content-Length: 100 + * Cache-Control: max-age=600 + * + * AES_256_WITH_MD5 + * 2 + * base64-encoded peerCertificate[0] + * base64-encoded peerCertificate[1] + * -1 + * + * The file is newline separated. The first two lines are the URL and + * the request method. Next is the number of HTTP Vary request header + * lines, followed by those lines. + * + * Next is the response status line, followed by the number of HTTP + * response header lines, followed by those lines. + * + * HTTPS responses also contain SSL session information. This begins + * with a blank line, and then a line containing the cipher suite. Next + * is the length of the peer certificate chain. These certificates are + * base64-encoded and appear each on their own line. The next line + * contains the length of the local certificate chain. These + * certificates are also base64-encoded and appear each on their own + * line. A length of -1 is used to encode a null array. */ - private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) { - return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) { - @Override public void close() throws IOException { - snapshot.close(); - super.close(); - } - }; + public Entry(InputStream in) throws IOException { + try { + StrictLineReader reader = new StrictLineReader(in, US_ASCII); + uri = reader.readLine(); + requestMethod = reader.readLine(); + varyHeaders = new RawHeaders(); + int varyRequestHeaderLineCount = reader.readInt(); + for (int i = 0; i < varyRequestHeaderLineCount; i++) { + varyHeaders.addLine(reader.readLine()); + } + + responseHeaders = new RawHeaders(); + responseHeaders.setStatusLine(reader.readLine()); + int responseHeaderLineCount = reader.readInt(); + for (int i = 0; i < responseHeaderLineCount; i++) { + responseHeaders.addLine(reader.readLine()); + } + + if (isHttps()) { + String blank = reader.readLine(); + if (!blank.isEmpty()) { + throw new IOException("expected \"\" but was \"" + blank + "\""); + } + cipherSuite = reader.readLine(); + peerCertificates = readCertArray(reader); + localCertificates = readCertArray(reader); + } else { + cipherSuite = null; + peerCertificates = null; + localCertificates = null; + } + } finally { + in.close(); + } } - static class EntryCacheResponse extends CacheResponse { - private final Entry entry; - private final DiskLruCache.Snapshot snapshot; - private final InputStream in; + public Entry(URI uri, RawHeaders varyHeaders, HttpURLConnection httpConnection) + throws IOException { + this.uri = uri.toString(); + this.varyHeaders = varyHeaders; + this.requestMethod = httpConnection.getRequestMethod(); + this.responseHeaders = RawHeaders.fromMultimap(httpConnection.getHeaderFields(), true); - public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { - this.entry = entry; - this.snapshot = snapshot; - this.in = newBodyInputStream(snapshot); - } - - @Override public Map> getHeaders() { - return entry.responseHeaders.toMultimap(true); - } - - @Override public InputStream getBody() { - return in; + if (isHttps()) { + HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection; + cipherSuite = httpsConnection.getCipherSuite(); + Certificate[] peerCertificatesNonFinal = null; + try { + peerCertificatesNonFinal = httpsConnection.getServerCertificates(); + } catch (SSLPeerUnverifiedException ignored) { } + peerCertificates = peerCertificatesNonFinal; + localCertificates = httpsConnection.getLocalCertificates(); + } else { + cipherSuite = null; + peerCertificates = null; + localCertificates = null; + } } - static class EntrySecureCacheResponse extends SecureCacheResponse { - private final Entry entry; - private final DiskLruCache.Snapshot snapshot; - private final InputStream in; + public void writeTo(DiskLruCache.Editor editor) throws IOException { + OutputStream out = editor.newOutputStream(ENTRY_METADATA); + Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8)); - public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { - this.entry = entry; - this.snapshot = snapshot; - this.in = newBodyInputStream(snapshot); - } + writer.write(uri + '\n'); + writer.write(requestMethod + '\n'); + writer.write(Integer.toString(varyHeaders.length()) + '\n'); + for (int i = 0; i < varyHeaders.length(); i++) { + writer.write(varyHeaders.getFieldName(i) + ": " + varyHeaders.getValue(i) + '\n'); + } - @Override public Map> getHeaders() { - return entry.responseHeaders.toMultimap(true); - } + writer.write(responseHeaders.getStatusLine() + '\n'); + writer.write(Integer.toString(responseHeaders.length()) + '\n'); + for (int i = 0; i < responseHeaders.length(); i++) { + writer.write(responseHeaders.getFieldName(i) + ": " + responseHeaders.getValue(i) + '\n'); + } - @Override public InputStream getBody() { - return in; - } - - @Override public String getCipherSuite() { - return entry.cipherSuite; - } - - @Override public List getServerCertificateChain() - throws SSLPeerUnverifiedException { - if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { - throw new SSLPeerUnverifiedException(null); - } - return Arrays.asList(entry.peerCertificates.clone()); - } - - @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { - if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { - throw new SSLPeerUnverifiedException(null); - } - return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal(); - } - - @Override public List getLocalCertificateChain() { - if (entry.localCertificates == null || entry.localCertificates.length == 0) { - return null; - } - return Arrays.asList(entry.localCertificates.clone()); - } - - @Override public Principal getLocalPrincipal() { - if (entry.localCertificates == null || entry.localCertificates.length == 0) { - return null; - } - return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal(); - } + if (isHttps()) { + writer.write('\n'); + writer.write(cipherSuite + '\n'); + writeCertArray(writer, peerCertificates); + writeCertArray(writer, localCertificates); + } + writer.close(); } + + private boolean isHttps() { + return uri.startsWith("https://"); + } + + private Certificate[] readCertArray(StrictLineReader reader) throws IOException { + int length = reader.readInt(); + if (length == -1) { + return null; + } + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + Certificate[] result = new Certificate[length]; + for (int i = 0; i < result.length; i++) { + String line = reader.readLine(); + byte[] bytes = Base64.decode(line.getBytes("US-ASCII")); + result[i] = certificateFactory.generateCertificate(new ByteArrayInputStream(bytes)); + } + return result; + } catch (CertificateException e) { + throw new IOException(e); + } + } + + private void writeCertArray(Writer writer, Certificate[] certificates) throws IOException { + if (certificates == null) { + writer.write("-1\n"); + return; + } + try { + writer.write(Integer.toString(certificates.length) + '\n'); + for (Certificate certificate : certificates) { + byte[] bytes = certificate.getEncoded(); + String line = Base64.encode(bytes); + writer.write(line + '\n'); + } + } catch (CertificateEncodingException e) { + throw new IOException(e); + } + } + + public boolean matches(URI uri, String requestMethod, + Map> requestHeaders) { + return this.uri.equals(uri.toString()) + && this.requestMethod.equals(requestMethod) + && new ResponseHeaders(uri, responseHeaders).varyMatches(varyHeaders.toMultimap(false), + requestHeaders); + } + } + + /** + * Returns an input stream that reads the body of a snapshot, closing the + * snapshot when the stream is closed. + */ + private static InputStream newBodyInputStream(final DiskLruCache.Snapshot snapshot) { + return new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) { + @Override public void close() throws IOException { + snapshot.close(); + super.close(); + } + }; + } + + static class EntryCacheResponse extends CacheResponse { + private final Entry entry; + private final DiskLruCache.Snapshot snapshot; + private final InputStream in; + + public EntryCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { + this.entry = entry; + this.snapshot = snapshot; + this.in = newBodyInputStream(snapshot); + } + + @Override public Map> getHeaders() { + return entry.responseHeaders.toMultimap(true); + } + + @Override public InputStream getBody() { + return in; + } + } + + static class EntrySecureCacheResponse extends SecureCacheResponse { + private final Entry entry; + private final DiskLruCache.Snapshot snapshot; + private final InputStream in; + + public EntrySecureCacheResponse(Entry entry, DiskLruCache.Snapshot snapshot) { + this.entry = entry; + this.snapshot = snapshot; + this.in = newBodyInputStream(snapshot); + } + + @Override public Map> getHeaders() { + return entry.responseHeaders.toMultimap(true); + } + + @Override public InputStream getBody() { + return in; + } + + @Override public String getCipherSuite() { + return entry.cipherSuite; + } + + @Override public List getServerCertificateChain() + throws SSLPeerUnverifiedException { + if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { + throw new SSLPeerUnverifiedException(null); + } + return Arrays.asList(entry.peerCertificates.clone()); + } + + @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + if (entry.peerCertificates == null || entry.peerCertificates.length == 0) { + throw new SSLPeerUnverifiedException(null); + } + return ((X509Certificate) entry.peerCertificates[0]).getSubjectX500Principal(); + } + + @Override public List getLocalCertificateChain() { + if (entry.localCertificates == null || entry.localCertificates.length == 0) { + return null; + } + return Arrays.asList(entry.localCertificates.clone()); + } + + @Override public Principal getLocalPrincipal() { + if (entry.localCertificates == null || entry.localCertificates.length == 0) { + return null; + } + return ((X509Certificate) entry.localCertificates[0]).getSubjectX500Principal(); + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java index eb72a25e8..d2f788eb9 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java @@ -18,7 +18,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.internal.Util; -import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -28,483 +27,474 @@ import java.net.CacheRequest; import java.net.ProtocolException; import java.net.Socket; +import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; + public final class HttpTransport implements Transport { - /** - * The maximum number of bytes to buffer when sending headers and a request - * body. When the headers and body can be sent in a single write, the - * request completes sooner. In one WiFi benchmark, using a large enough - * buffer sped up some uploads by half. - */ - private static final int MAX_REQUEST_BUFFER_LENGTH = 32768; + /** + * The maximum number of bytes to buffer when sending headers and a request + * body. When the headers and body can be sent in a single write, the + * request completes sooner. In one WiFi benchmark, using a large enough + * buffer sped up some uploads by half. + */ + private static final int MAX_REQUEST_BUFFER_LENGTH = 32768; - /** - * The timeout to use while discarding a stream of input data. Since this is - * used for connection reuse, this timeout should be significantly less than - * the time it takes to establish a new connection. - */ - private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 30; + /** + * The timeout to use while discarding a stream of input data. Since this is + * used for connection reuse, this timeout should be significantly less than + * the time it takes to establish a new connection. + */ + private static final int DISCARD_STREAM_TIMEOUT_MILLIS = 30; - public static final int DEFAULT_CHUNK_LENGTH = 1024; + public static final int DEFAULT_CHUNK_LENGTH = 1024; - private final HttpEngine httpEngine; - private final InputStream socketIn; - private final OutputStream socketOut; + private final HttpEngine httpEngine; + private final InputStream socketIn; + private final OutputStream socketOut; - /** - * This stream buffers the request headers and the request body when their - * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them - * we can save socket writes, which in turn saves a packet transmission. - * This is socketOut if the request size is large or unknown. - */ - private OutputStream requestOut; + /** + * This stream buffers the request headers and the request body when their + * combined size is less than MAX_REQUEST_BUFFER_LENGTH. By combining them + * we can save socket writes, which in turn saves a packet transmission. + * This is socketOut if the request size is large or unknown. + */ + private OutputStream requestOut; - public HttpTransport(HttpEngine httpEngine, - OutputStream outputStream, InputStream inputStream) { - this.httpEngine = httpEngine; - this.socketOut = outputStream; - this.requestOut = outputStream; - this.socketIn = inputStream; + public HttpTransport(HttpEngine httpEngine, OutputStream outputStream, InputStream inputStream) { + this.httpEngine = httpEngine; + this.socketOut = outputStream; + this.requestOut = outputStream; + this.socketIn = inputStream; + } + + @Override public OutputStream createRequestBody() throws IOException { + boolean chunked = httpEngine.requestHeaders.isChunked(); + if (!chunked + && httpEngine.policy.getChunkLength() > 0 + && httpEngine.connection.getHttpMinorVersion() != 0) { + httpEngine.requestHeaders.setChunked(); + chunked = true; } - @Override public OutputStream createRequestBody() throws IOException { - boolean chunked = httpEngine.requestHeaders.isChunked(); - if (!chunked - && httpEngine.policy.getChunkLength() > 0 - && httpEngine.connection.getHttpMinorVersion() != 0) { - httpEngine.requestHeaders.setChunked(); - chunked = true; - } - - // Stream a request body of unknown length. - if (chunked) { - int chunkLength = httpEngine.policy.getChunkLength(); - if (chunkLength == -1) { - chunkLength = DEFAULT_CHUNK_LENGTH; - } - writeRequestHeaders(); - return new ChunkedOutputStream(requestOut, chunkLength); - } - - // Stream a request body of a known length. - int fixedContentLength = httpEngine.policy.getFixedContentLength(); - if (fixedContentLength != -1) { - httpEngine.requestHeaders.setContentLength(fixedContentLength); - writeRequestHeaders(); - return new FixedLengthOutputStream(requestOut, fixedContentLength); - } - - // Buffer a request body of a known length. - int contentLength = httpEngine.requestHeaders.getContentLength(); - if (contentLength != -1) { - writeRequestHeaders(); - return new RetryableOutputStream(contentLength); - } - - // Buffer a request body of an unknown length. Don't write request - // headers until the entire body is ready; otherwise we can't set the - // Content-Length header correctly. - return new RetryableOutputStream(); + // Stream a request body of unknown length. + if (chunked) { + int chunkLength = httpEngine.policy.getChunkLength(); + if (chunkLength == -1) { + chunkLength = DEFAULT_CHUNK_LENGTH; + } + writeRequestHeaders(); + return new ChunkedOutputStream(requestOut, chunkLength); } - @Override public void flushRequest() throws IOException { - requestOut.flush(); - requestOut = socketOut; + // Stream a request body of a known length. + int fixedContentLength = httpEngine.policy.getFixedContentLength(); + if (fixedContentLength != -1) { + httpEngine.requestHeaders.setContentLength(fixedContentLength); + writeRequestHeaders(); + return new FixedLengthOutputStream(requestOut, fixedContentLength); } - @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { - requestBody.writeToSocket(requestOut); + // Buffer a request body of a known length. + int contentLength = httpEngine.requestHeaders.getContentLength(); + if (contentLength != -1) { + writeRequestHeaders(); + return new RetryableOutputStream(contentLength); } - /** - * Prepares the HTTP headers and sends them to the server. - * - *

For streaming requests with a body, headers must be prepared - * before the output stream has been written to. Otherwise - * the body would need to be buffered! - * - *

For non-streaming requests with a body, headers must be prepared - * after the output stream has been written to and closed. - * This ensures that the {@code Content-Length} header field receives the - * proper value. - */ - public void writeRequestHeaders() throws IOException { - httpEngine.writingRequestHeaders(); - int contentLength = httpEngine.requestHeaders.getContentLength(); - RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders(); - byte[] bytes = headersToSend.toBytes(); + // Buffer a request body of an unknown length. Don't write request + // headers until the entire body is ready; otherwise we can't set the + // Content-Length header correctly. + return new RetryableOutputStream(); + } - if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) { - requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength); - } + @Override public void flushRequest() throws IOException { + requestOut.flush(); + requestOut = socketOut; + } - requestOut.write(bytes); + @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { + requestBody.writeToSocket(requestOut); + } + + /** + * Prepares the HTTP headers and sends them to the server. + * + *

For streaming requests with a body, headers must be prepared + * before the output stream has been written to. Otherwise + * the body would need to be buffered! + * + *

For non-streaming requests with a body, headers must be prepared + * after the output stream has been written to and closed. + * This ensures that the {@code Content-Length} header field receives the + * proper value. + */ + public void writeRequestHeaders() throws IOException { + httpEngine.writingRequestHeaders(); + int contentLength = httpEngine.requestHeaders.getContentLength(); + RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders(); + byte[] bytes = headersToSend.toBytes(); + + if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) { + requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength); } - @Override public ResponseHeaders readResponseHeaders() throws IOException { - RawHeaders headers = RawHeaders.fromBytes(socketIn); - httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion()); - httpEngine.receiveHeaders(headers); - return new ResponseHeaders(httpEngine.uri, headers); + requestOut.write(bytes); + } + + @Override public ResponseHeaders readResponseHeaders() throws IOException { + RawHeaders headers = RawHeaders.fromBytes(socketIn); + httpEngine.connection.setHttpMinorVersion(headers.getHttpMinorVersion()); + httpEngine.receiveHeaders(headers); + return new ResponseHeaders(httpEngine.uri, headers); + } + + public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut, + InputStream responseBodyIn) { + if (streamCancelled) { + return false; } - public boolean makeReusable(boolean streamCancelled, - OutputStream requestBodyOut, InputStream responseBodyIn) { - if (streamCancelled) { - return false; - } + // We cannot reuse sockets that have incomplete output. + if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) { + return false; + } - // We cannot reuse sockets that have incomplete output. - if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) { - return false; - } + // If the request specified that the connection shouldn't be reused, don't reuse it. + if (httpEngine.requestHeaders.hasConnectionClose()) { + return false; + } - // If the request specified that the connection shouldn't be reused, don't reuse it. - if (httpEngine.requestHeaders.hasConnectionClose()) { - return false; - } + // If the response specified that the connection shouldn't be reused, don't reuse it. + if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) { + return false; + } - // If the response specified that the connection shouldn't be reused, don't reuse it. - if (httpEngine.responseHeaders != null && httpEngine.responseHeaders.hasConnectionClose()) { - return false; - } + if (responseBodyIn instanceof UnknownLengthHttpInputStream) { + return false; + } - if (responseBodyIn instanceof UnknownLengthHttpInputStream) { - return false; - } + if (responseBodyIn != null) { + return discardStream(httpEngine, responseBodyIn); + } - if (responseBodyIn != null) { - return discardStream(httpEngine, responseBodyIn); - } + return true; + } + /** + * Discards the response body so that the connection can be reused. This + * needs to be done judiciously, since it delays the current request in + * order to speed up a potential future request that may never occur. + */ + private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) { + Connection connection = httpEngine.connection; + if (connection == null) return false; + Socket socket = connection.getSocket(); + if (socket == null) return false; + try { + int socketTimeout = socket.getSoTimeout(); + socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS); + try { + Util.skipAll(responseBodyIn); return true; + } finally { + socket.setSoTimeout(socketTimeout); + } + } catch (IOException e) { + return false; + } + } + + @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { + if (!httpEngine.hasResponseBody()) { + return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0); } - /** - * Discards the response body so that the connection can be reused. This - * needs to be done judiciously, since it delays the current request in - * order to speed up a potential future request that may never occur. - */ - private static boolean discardStream(HttpEngine httpEngine, InputStream responseBodyIn) { - Connection connection = httpEngine.connection; - if (connection == null) return false; - Socket socket = connection.getSocket(); - if (socket == null) return false; - try { - int socketTimeout = socket.getSoTimeout(); - socket.setSoTimeout(DISCARD_STREAM_TIMEOUT_MILLIS); - try { - Util.skipAll(responseBodyIn); - return true; - } finally { - socket.setSoTimeout(socketTimeout); - } - } catch (IOException e) { - return false; - } + if (httpEngine.responseHeaders.isChunked()) { + return new ChunkedInputStream(socketIn, cacheRequest, this); } - @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { - if (!httpEngine.hasResponseBody()) { - return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0); - } - - if (httpEngine.responseHeaders.isChunked()) { - return new ChunkedInputStream(socketIn, cacheRequest, this); - } - - if (httpEngine.responseHeaders.getContentLength() != -1) { - return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, - httpEngine.responseHeaders.getContentLength()); - } + if (httpEngine.responseHeaders.getContentLength() != -1) { + return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, + httpEngine.responseHeaders.getContentLength()); + } /* * Wrap the input stream from the connection (rather than just returning * "socketIn" directly here), so that we can control its use after the * reference escapes. */ - return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine); + return new UnknownLengthHttpInputStream(socketIn, cacheRequest, httpEngine); + } + + /** An HTTP body with a fixed length known in advance. */ + private static final class FixedLengthOutputStream extends AbstractHttpOutputStream { + private final OutputStream socketOut; + private int bytesRemaining; + + private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) { + this.socketOut = socketOut; + this.bytesRemaining = bytesRemaining; + } + + @Override public void write(byte[] buffer, int offset, int count) throws IOException { + checkNotClosed(); + checkOffsetAndCount(buffer.length, offset, count); + if (count > bytesRemaining) { + throw new ProtocolException("expected " + bytesRemaining + " bytes but received " + count); + } + socketOut.write(buffer, offset, count); + bytesRemaining -= count; + } + + @Override public void flush() throws IOException { + if (closed) { + return; // don't throw; this stream might have been closed on the caller's behalf + } + socketOut.flush(); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + closed = true; + if (bytesRemaining > 0) { + throw new ProtocolException("unexpected end of stream"); + } + } + } + + /** + * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are + * buffered until {@code maxChunkLength} bytes are ready, at which point the + * chunk is written and the buffer is cleared. + */ + private static final class ChunkedOutputStream extends AbstractHttpOutputStream { + private static final byte[] CRLF = { '\r', '\n' }; + private static final byte[] HEX_DIGITS = + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' }; + + /** Scratch space for up to 8 hex digits, and then a constant CRLF. */ + private final byte[] hex = { 0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n' }; + + private final OutputStream socketOut; + private final int maxChunkLength; + private final ByteArrayOutputStream bufferedChunk; + + private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) { + this.socketOut = socketOut; + this.maxChunkLength = Math.max(1, dataLength(maxChunkLength)); + this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength); } /** - * An HTTP body with a fixed length known in advance. + * Returns the amount of data that can be transmitted in a chunk whose total + * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably + * useful to match sizes with wire-protocol packets. */ - private static final class FixedLengthOutputStream extends AbstractHttpOutputStream { - private final OutputStream socketOut; - private int bytesRemaining; - - private FixedLengthOutputStream(OutputStream socketOut, int bytesRemaining) { - this.socketOut = socketOut; - this.bytesRemaining = bytesRemaining; - } - - @Override public void write(byte[] buffer, int offset, int count) throws IOException { - checkNotClosed(); - checkOffsetAndCount(buffer.length, offset, count); - if (count > bytesRemaining) { - throw new ProtocolException("expected " + bytesRemaining - + " bytes but received " + count); - } - socketOut.write(buffer, offset, count); - bytesRemaining -= count; - } - - @Override public void flush() throws IOException { - if (closed) { - return; // don't throw; this stream might have been closed on the caller's behalf - } - socketOut.flush(); - } - - @Override public void close() throws IOException { - if (closed) { - return; - } - closed = true; - if (bytesRemaining > 0) { - throw new ProtocolException("unexpected end of stream"); - } - } + private int dataLength(int dataPlusHeaderLength) { + int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data + for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) { + headerLength++; + } + return dataPlusHeaderLength - headerLength; } - /** - * An HTTP body with alternating chunk sizes and chunk bodies. Chunks are - * buffered until {@code maxChunkLength} bytes are ready, at which point the - * chunk is written and the buffer is cleared. - */ - private static final class ChunkedOutputStream extends AbstractHttpOutputStream { - private static final byte[] CRLF = {'\r', '\n'}; - private static final byte[] HEX_DIGITS = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' - }; - private static final byte[] FINAL_CHUNK = new byte[] {'0', '\r', '\n', '\r', '\n'}; + @Override public synchronized void write(byte[] buffer, int offset, int count) + throws IOException { + checkNotClosed(); + checkOffsetAndCount(buffer.length, offset, count); - /** Scratch space for up to 8 hex digits, and then a constant CRLF. */ - private final byte[] hex = {0, 0, 0, 0, 0, 0, 0, 0, '\r', '\n'}; + while (count > 0) { + int numBytesWritten; - private final OutputStream socketOut; - private final int maxChunkLength; - private final ByteArrayOutputStream bufferedChunk; - - private ChunkedOutputStream(OutputStream socketOut, int maxChunkLength) { - this.socketOut = socketOut; - this.maxChunkLength = Math.max(1, dataLength(maxChunkLength)); - this.bufferedChunk = new ByteArrayOutputStream(maxChunkLength); - } - - /** - * Returns the amount of data that can be transmitted in a chunk whose total - * length (data+headers) is {@code dataPlusHeaderLength}. This is presumably - * useful to match sizes with wire-protocol packets. - */ - private int dataLength(int dataPlusHeaderLength) { - int headerLength = 4; // "\r\n" after the size plus another "\r\n" after the data - for (int i = dataPlusHeaderLength - headerLength; i > 0; i >>= 4) { - headerLength++; - } - return dataPlusHeaderLength - headerLength; - } - - @Override public synchronized void write(byte[] buffer, int offset, int count) - throws IOException { - checkNotClosed(); - checkOffsetAndCount(buffer.length, offset, count); - - while (count > 0) { - int numBytesWritten; - - if (bufferedChunk.size() > 0 || count < maxChunkLength) { - // fill the buffered chunk and then maybe write that to the stream - numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size()); - // TODO: skip unnecessary copies from buffer->bufferedChunk? - bufferedChunk.write(buffer, offset, numBytesWritten); - if (bufferedChunk.size() == maxChunkLength) { - writeBufferedChunkToSocket(); - } - - } else { - // write a single chunk of size maxChunkLength to the stream - numBytesWritten = maxChunkLength; - writeHex(numBytesWritten); - socketOut.write(buffer, offset, numBytesWritten); - socketOut.write(CRLF); - } - - offset += numBytesWritten; - count -= numBytesWritten; - } - } - - /** - * Equivalent to, but cheaper than writing Integer.toHexString().getBytes() - * followed by CRLF. - */ - private void writeHex(int i) throws IOException { - int cursor = 8; - do { - hex[--cursor] = HEX_DIGITS[i & 0xf]; - } while ((i >>>= 4) != 0); - socketOut.write(hex, cursor, hex.length - cursor); - } - - @Override public synchronized void flush() throws IOException { - if (closed) { - return; // don't throw; this stream might have been closed on the caller's behalf - } + if (bufferedChunk.size() > 0 || count < maxChunkLength) { + // fill the buffered chunk and then maybe write that to the stream + numBytesWritten = Math.min(count, maxChunkLength - bufferedChunk.size()); + // TODO: skip unnecessary copies from buffer->bufferedChunk? + bufferedChunk.write(buffer, offset, numBytesWritten); + if (bufferedChunk.size() == maxChunkLength) { writeBufferedChunkToSocket(); - socketOut.flush(); + } + } else { + // write a single chunk of size maxChunkLength to the stream + numBytesWritten = maxChunkLength; + writeHex(numBytesWritten); + socketOut.write(buffer, offset, numBytesWritten); + socketOut.write(CRLF); } - @Override public synchronized void close() throws IOException { - if (closed) { - return; - } - closed = true; - writeBufferedChunkToSocket(); - socketOut.write(FINAL_CHUNK); - } - - private void writeBufferedChunkToSocket() throws IOException { - int size = bufferedChunk.size(); - if (size <= 0) { - return; - } - - writeHex(size); - bufferedChunk.writeTo(socketOut); - bufferedChunk.reset(); - socketOut.write(CRLF); - } + offset += numBytesWritten; + count -= numBytesWritten; + } } /** - * An HTTP body with a fixed length specified in advance. + * Equivalent to, but cheaper than writing Integer.toHexString().getBytes() + * followed by CRLF. */ - private static class FixedLengthInputStream extends AbstractHttpInputStream { - private int bytesRemaining; - - public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, - HttpEngine httpEngine, int length) throws IOException { - super(is, httpEngine, cacheRequest); - bytesRemaining = length; - if (bytesRemaining == 0) { - endOfInput(false); - } - } - - @Override public int read(byte[] buffer, int offset, int count) throws IOException { - checkOffsetAndCount(buffer.length, offset, count); - checkNotClosed(); - if (bytesRemaining == 0) { - return -1; - } - int read = in.read(buffer, offset, Math.min(count, bytesRemaining)); - if (read == -1) { - unexpectedEndOfInput(); // the server didn't supply the promised content length - throw new ProtocolException("unexpected end of stream"); - } - bytesRemaining -= read; - cacheWrite(buffer, offset, read); - if (bytesRemaining == 0) { - endOfInput(false); - } - return read; - } - - @Override public int available() throws IOException { - checkNotClosed(); - return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining); - } - - @Override public void close() throws IOException { - if (closed) { - return; - } - if (bytesRemaining != 0 && !discardStream(httpEngine, this)) { - unexpectedEndOfInput(); - } - closed = true; - } + private void writeHex(int i) throws IOException { + int cursor = 8; + do { + hex[--cursor] = HEX_DIGITS[i & 0xf]; + } while ((i >>>= 4) != 0); + socketOut.write(hex, cursor, hex.length - cursor); } - /** - * An HTTP body with alternating chunk sizes and chunk bodies. - */ - private static class ChunkedInputStream extends AbstractHttpInputStream { - private static final int NO_CHUNK_YET = -1; - private final HttpTransport transport; - private int bytesRemainingInChunk = NO_CHUNK_YET; - private boolean hasMoreChunks = true; - - ChunkedInputStream(InputStream is, CacheRequest cacheRequest, - HttpTransport transport) throws IOException { - super(is, transport.httpEngine, cacheRequest); - this.transport = transport; - } - - @Override public int read(byte[] buffer, int offset, int count) throws IOException { - checkOffsetAndCount(buffer.length, offset, count); - checkNotClosed(); - - if (!hasMoreChunks) { - return -1; - } - if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) { - readChunkSize(); - if (!hasMoreChunks) { - return -1; - } - } - int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk)); - if (read == -1) { - unexpectedEndOfInput(); // the server didn't supply the promised chunk length - throw new IOException("unexpected end of stream"); - } - bytesRemainingInChunk -= read; - cacheWrite(buffer, offset, read); - return read; - } - - private void readChunkSize() throws IOException { - // read the suffix of the previous chunk - if (bytesRemainingInChunk != NO_CHUNK_YET) { - Util.readAsciiLine(in); - } - String chunkSizeString = Util.readAsciiLine(in); - int index = chunkSizeString.indexOf(";"); - if (index != -1) { - chunkSizeString = chunkSizeString.substring(0, index); - } - try { - bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16); - } catch (NumberFormatException e) { - throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString); - } - if (bytesRemainingInChunk == 0) { - hasMoreChunks = false; - RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders(); - RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders); - httpEngine.receiveHeaders(rawResponseHeaders); - endOfInput(false); - } - } - - @Override public int available() throws IOException { - checkNotClosed(); - if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) { - return 0; - } - return Math.min(in.available(), bytesRemainingInChunk); - } - - @Override public void close() throws IOException { - if (closed) { - return; - } - if (hasMoreChunks && !discardStream(httpEngine, this)) { - unexpectedEndOfInput(); - } - closed = true; - } + @Override public synchronized void flush() throws IOException { + if (closed) { + return; // don't throw; this stream might have been closed on the caller's behalf + } + writeBufferedChunkToSocket(); + socketOut.flush(); } + @Override public synchronized void close() throws IOException { + if (closed) { + return; + } + closed = true; + writeBufferedChunkToSocket(); + socketOut.write(FINAL_CHUNK); + } + + private void writeBufferedChunkToSocket() throws IOException { + int size = bufferedChunk.size(); + if (size <= 0) { + return; + } + + writeHex(size); + bufferedChunk.writeTo(socketOut); + bufferedChunk.reset(); + socketOut.write(CRLF); + } + } + + /** An HTTP body with a fixed length specified in advance. */ + private static class FixedLengthInputStream extends AbstractHttpInputStream { + private int bytesRemaining; + + public FixedLengthInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine, + int length) throws IOException { + super(is, httpEngine, cacheRequest); + bytesRemaining = length; + if (bytesRemaining == 0) { + endOfInput(false); + } + } + + @Override public int read(byte[] buffer, int offset, int count) throws IOException { + checkOffsetAndCount(buffer.length, offset, count); + checkNotClosed(); + if (bytesRemaining == 0) { + return -1; + } + int read = in.read(buffer, offset, Math.min(count, bytesRemaining)); + if (read == -1) { + unexpectedEndOfInput(); // the server didn't supply the promised content length + throw new ProtocolException("unexpected end of stream"); + } + bytesRemaining -= read; + cacheWrite(buffer, offset, read); + if (bytesRemaining == 0) { + endOfInput(false); + } + return read; + } + + @Override public int available() throws IOException { + checkNotClosed(); + return bytesRemaining == 0 ? 0 : Math.min(in.available(), bytesRemaining); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + if (bytesRemaining != 0 && !discardStream(httpEngine, this)) { + unexpectedEndOfInput(); + } + closed = true; + } + } + + /** An HTTP body with alternating chunk sizes and chunk bodies. */ + private static class ChunkedInputStream extends AbstractHttpInputStream { + private static final int NO_CHUNK_YET = -1; + private final HttpTransport transport; + private int bytesRemainingInChunk = NO_CHUNK_YET; + private boolean hasMoreChunks = true; + + ChunkedInputStream(InputStream is, CacheRequest cacheRequest, HttpTransport transport) + throws IOException { + super(is, transport.httpEngine, cacheRequest); + this.transport = transport; + } + + @Override public int read(byte[] buffer, int offset, int count) throws IOException { + checkOffsetAndCount(buffer.length, offset, count); + checkNotClosed(); + + if (!hasMoreChunks) { + return -1; + } + if (bytesRemainingInChunk == 0 || bytesRemainingInChunk == NO_CHUNK_YET) { + readChunkSize(); + if (!hasMoreChunks) { + return -1; + } + } + int read = in.read(buffer, offset, Math.min(count, bytesRemainingInChunk)); + if (read == -1) { + unexpectedEndOfInput(); // the server didn't supply the promised chunk length + throw new IOException("unexpected end of stream"); + } + bytesRemainingInChunk -= read; + cacheWrite(buffer, offset, read); + return read; + } + + private void readChunkSize() throws IOException { + // read the suffix of the previous chunk + if (bytesRemainingInChunk != NO_CHUNK_YET) { + Util.readAsciiLine(in); + } + String chunkSizeString = Util.readAsciiLine(in); + int index = chunkSizeString.indexOf(";"); + if (index != -1) { + chunkSizeString = chunkSizeString.substring(0, index); + } + try { + bytesRemainingInChunk = Integer.parseInt(chunkSizeString.trim(), 16); + } catch (NumberFormatException e) { + throw new ProtocolException("Expected a hex chunk size but was " + chunkSizeString); + } + if (bytesRemainingInChunk == 0) { + hasMoreChunks = false; + RawHeaders rawResponseHeaders = httpEngine.responseHeaders.getHeaders(); + RawHeaders.readHeaders(transport.socketIn, rawResponseHeaders); + httpEngine.receiveHeaders(rawResponseHeaders); + endOfInput(false); + } + } + + @Override public int available() throws IOException { + checkNotClosed(); + if (!hasMoreChunks || bytesRemainingInChunk == NO_CHUNK_YET) { + return 0; + } + return Math.min(in.available(), bytesRemainingInChunk); + } + + @Override public void close() throws IOException { + if (closed) { + return; + } + if (hasMoreChunks && !discardStream(httpEngine, this)) { + unexpectedEndOfInput(); + } + closed = true; + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java index 10773a6d1..90df6db51 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java @@ -20,7 +20,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.internal.Util; -import static com.squareup.okhttp.internal.Util.getEffectivePort; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; @@ -41,6 +40,8 @@ import java.util.List; import java.util.Map; import javax.net.ssl.SSLHandshakeException; +import static com.squareup.okhttp.internal.Util.getEffectivePort; + /** * This implementation uses HttpEngine to send requests and receive responses. * This class may use multiple HttpEngines to follow redirects, authentication @@ -56,137 +57,133 @@ import javax.net.ssl.SSLHandshakeException; * is currently connected to a server. */ public class HttpURLConnectionImpl extends HttpURLConnection { - /** - * How many redirects should we follow? Chrome follows 21; Firefox, curl, - * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. - */ - private static final int MAX_REDIRECTS = 20; + /** + * How many redirects should we follow? Chrome follows 21; Firefox, curl, + * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5. + */ + private static final int MAX_REDIRECTS = 20; - private final int defaultPort; + private final int defaultPort; - private Proxy proxy; - final ProxySelector proxySelector; - final CookieHandler cookieHandler; - final ResponseCache responseCache; - final ConnectionPool connectionPool; + private Proxy proxy; + final ProxySelector proxySelector; + final CookieHandler cookieHandler; + final ResponseCache responseCache; + final ConnectionPool connectionPool; - private final RawHeaders rawRequestHeaders = new RawHeaders(); + private final RawHeaders rawRequestHeaders = new RawHeaders(); - private int redirectionCount; + private int redirectionCount; - protected IOException httpEngineFailure; - protected HttpEngine httpEngine; + protected IOException httpEngineFailure; + protected HttpEngine httpEngine; - public HttpURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector, - CookieHandler cookieHandler, ResponseCache responseCache, - ConnectionPool connectionPool) { - super(url); - this.defaultPort = defaultPort; - this.proxy = proxy; - this.proxySelector = proxySelector; - this.cookieHandler = cookieHandler; - this.responseCache = responseCache; - this.connectionPool = connectionPool; + public HttpURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector, + CookieHandler cookieHandler, ResponseCache responseCache, ConnectionPool connectionPool) { + super(url); + this.defaultPort = defaultPort; + this.proxy = proxy; + this.proxySelector = proxySelector; + this.cookieHandler = cookieHandler; + this.responseCache = responseCache; + this.connectionPool = connectionPool; + } + + @Override public final void connect() throws IOException { + initHttpEngine(); + boolean success; + do { + success = execute(false); + } while (!success); + } + + @Override public final void disconnect() { + // Calling disconnect() before a connection exists should have no effect. + if (httpEngine != null) { + // We close the response body here instead of in + // HttpEngine.release because that is called when input + // has been completely read from the underlying socket. + // However the response body can be a GZIPInputStream that + // still has unread data. + if (httpEngine.hasResponse()) { + Util.closeQuietly(httpEngine.getResponseBody()); + } + httpEngine.release(true); + } + } + + /** + * Returns an input stream from the server in the case of error such as the + * requested file (txt, htm, html) is not found on the remote server. + */ + @Override public final InputStream getErrorStream() { + try { + HttpEngine response = getResponse(); + if (response.hasResponseBody() && response.getResponseCode() >= HTTP_BAD_REQUEST) { + return response.getResponseBody(); + } + return null; + } catch (IOException e) { + return null; + } + } + + /** + * Returns the value of the field at {@code position}. Returns null if there + * are fewer than {@code position} headers. + */ + @Override public final String getHeaderField(int position) { + try { + return getResponse().getResponseHeaders().getHeaders().getValue(position); + } catch (IOException e) { + return null; + } + } + + /** + * Returns the value of the field corresponding to the {@code fieldName}, or + * null if there is no such field. If the field has multiple values, the + * last value is returned. + */ + @Override public final String getHeaderField(String fieldName) { + try { + RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders(); + return fieldName == null ? rawHeaders.getStatusLine() : rawHeaders.get(fieldName); + } catch (IOException e) { + return null; + } + } + + @Override public final String getHeaderFieldKey(int position) { + try { + return getResponse().getResponseHeaders().getHeaders().getFieldName(position); + } catch (IOException e) { + return null; + } + } + + @Override public final Map> getHeaderFields() { + try { + return getResponse().getResponseHeaders().getHeaders().toMultimap(true); + } catch (IOException e) { + return null; + } + } + + @Override public final Map> getRequestProperties() { + if (connected) { + throw new IllegalStateException( + "Cannot access request header fields after connection is set"); + } + return rawRequestHeaders.toMultimap(false); + } + + @Override public final InputStream getInputStream() throws IOException { + if (!doInput) { + throw new ProtocolException("This protocol does not support input"); } - @Override public final void connect() throws IOException { - initHttpEngine(); - boolean success; - do { - success = execute(false); - } while (!success); - } - - @Override public final void disconnect() { - // Calling disconnect() before a connection exists should have no effect. - if (httpEngine != null) { - // We close the response body here instead of in - // HttpEngine.release because that is called when input - // has been completely read from the underlying socket. - // However the response body can be a GZIPInputStream that - // still has unread data. - if (httpEngine.hasResponse()) { - Util.closeQuietly(httpEngine.getResponseBody()); - } - httpEngine.release(true); - } - } - - /** - * Returns an input stream from the server in the case of error such as the - * requested file (txt, htm, html) is not found on the remote server. - */ - @Override public final InputStream getErrorStream() { - try { - HttpEngine response = getResponse(); - if (response.hasResponseBody() - && response.getResponseCode() >= HTTP_BAD_REQUEST) { - return response.getResponseBody(); - } - return null; - } catch (IOException e) { - return null; - } - } - - /** - * Returns the value of the field at {@code position}. Returns null if there - * are fewer than {@code position} headers. - */ - @Override public final String getHeaderField(int position) { - try { - return getResponse().getResponseHeaders().getHeaders().getValue(position); - } catch (IOException e) { - return null; - } - } - - /** - * Returns the value of the field corresponding to the {@code fieldName}, or - * null if there is no such field. If the field has multiple values, the - * last value is returned. - */ - @Override public final String getHeaderField(String fieldName) { - try { - RawHeaders rawHeaders = getResponse().getResponseHeaders().getHeaders(); - return fieldName == null - ? rawHeaders.getStatusLine() - : rawHeaders.get(fieldName); - } catch (IOException e) { - return null; - } - } - - @Override public final String getHeaderFieldKey(int position) { - try { - return getResponse().getResponseHeaders().getHeaders().getFieldName(position); - } catch (IOException e) { - return null; - } - } - - @Override public final Map> getHeaderFields() { - try { - return getResponse().getResponseHeaders().getHeaders().toMultimap(true); - } catch (IOException e) { - return null; - } - } - - @Override public final Map> getRequestProperties() { - if (connected) { - throw new IllegalStateException( - "Cannot access request header fields after connection is set"); - } - return rawRequestHeaders.toMultimap(false); - } - - @Override public final InputStream getInputStream() throws IOException { - if (!doInput) { - throw new ProtocolException("This protocol does not support input"); - } - - HttpEngine response = getResponse(); + HttpEngine response = getResponse(); /* * if the requested file does not exist, throw an exception formerly the @@ -194,302 +191,299 @@ public class HttpURLConnectionImpl extends HttpURLConnection { * text/html this has changed to return FileNotFoundException for all * file types */ - if (getResponseCode() >= HTTP_BAD_REQUEST) { - throw new FileNotFoundException(url.toString()); + if (getResponseCode() >= HTTP_BAD_REQUEST) { + throw new FileNotFoundException(url.toString()); + } + + InputStream result = response.getResponseBody(); + if (result == null) { + throw new ProtocolException("No response body exists; responseCode=" + getResponseCode()); + } + return result; + } + + @Override public final OutputStream getOutputStream() throws IOException { + connect(); + + OutputStream result = httpEngine.getRequestBody(); + if (result == null) { + throw new ProtocolException("method does not support a request body: " + method); + } else if (httpEngine.hasResponse()) { + throw new ProtocolException("cannot write request body after response has been read"); + } + + return result; + } + + @Override public final Permission getPermission() throws IOException { + String connectToAddress = getConnectToHost() + ":" + getConnectToPort(); + return new SocketPermission(connectToAddress, "connect, resolve"); + } + + private String getConnectToHost() { + return usingProxy() ? ((InetSocketAddress) proxy.address()).getHostName() : getURL().getHost(); + } + + private int getConnectToPort() { + int hostPort = + usingProxy() ? ((InetSocketAddress) proxy.address()).getPort() : getURL().getPort(); + return hostPort < 0 ? getDefaultPort() : hostPort; + } + + @Override public final String getRequestProperty(String field) { + if (field == null) { + return null; + } + return rawRequestHeaders.get(field); + } + + private void initHttpEngine() throws IOException { + if (httpEngineFailure != null) { + throw httpEngineFailure; + } else if (httpEngine != null) { + return; + } + + connected = true; + try { + if (doOutput) { + if (method.equals("GET")) { + // they are requesting a stream to write to. This implies a POST method + method = "POST"; + } else if (!method.equals("POST") && !method.equals("PUT")) { + // If the request method is neither POST nor PUT, then you're not writing + throw new ProtocolException(method + " does not support writing"); } + } + httpEngine = newHttpEngine(method, rawRequestHeaders, null, null); + } catch (IOException e) { + httpEngineFailure = e; + throw e; + } + } - InputStream result = response.getResponseBody(); - if (result == null) { - throw new ProtocolException("No response body exists; responseCode=" - + getResponseCode()); - } - return result; + /** + * Create a new HTTP engine. This hook method is non-final so it can be + * overridden by HttpsURLConnectionImpl. + */ + protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, + Connection connection, RetryableOutputStream requestBody) throws IOException { + return new HttpEngine(this, method, requestHeaders, connection, requestBody); + } + + /** + * Aggressively tries to get the final HTTP response, potentially making + * many HTTP requests in the process in order to cope with redirects and + * authentication. + */ + private HttpEngine getResponse() throws IOException { + initHttpEngine(); + + if (httpEngine.hasResponse()) { + return httpEngine; } - @Override public final OutputStream getOutputStream() throws IOException { - connect(); + while (true) { + if (!execute(true)) { + continue; + } - OutputStream result = httpEngine.getRequestBody(); - if (result == null) { - throw new ProtocolException("method does not support a request body: " + method); - } else if (httpEngine.hasResponse()) { - throw new ProtocolException("cannot write request body after response has been read"); - } - - return result; - } - - @Override public final Permission getPermission() throws IOException { - String connectToAddress = getConnectToHost() + ":" + getConnectToPort(); - return new SocketPermission(connectToAddress, "connect, resolve"); - } - - private String getConnectToHost() { - return usingProxy() - ? ((InetSocketAddress) proxy.address()).getHostName() - : getURL().getHost(); - } - - private int getConnectToPort() { - int hostPort = usingProxy() - ? ((InetSocketAddress) proxy.address()).getPort() - : getURL().getPort(); - return hostPort < 0 ? getDefaultPort() : hostPort; - } - - @Override public final String getRequestProperty(String field) { - if (field == null) { - return null; - } - return rawRequestHeaders.get(field); - } - - private void initHttpEngine() throws IOException { - if (httpEngineFailure != null) { - throw httpEngineFailure; - } else if (httpEngine != null) { - return; - } - - connected = true; - try { - if (doOutput) { - if (method.equals("GET")) { - // they are requesting a stream to write to. This implies a POST method - method = "POST"; - } else if (!method.equals("POST") && !method.equals("PUT")) { - // If the request method is neither POST nor PUT, then you're not writing - throw new ProtocolException(method + " does not support writing"); - } - } - httpEngine = newHttpEngine(method, rawRequestHeaders, null, null); - } catch (IOException e) { - httpEngineFailure = e; - throw e; - } - } - - /** - * Create a new HTTP engine. This hook method is non-final so it can be - * overridden by HttpsURLConnectionImpl. - */ - protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, - Connection connection, RetryableOutputStream requestBody) throws IOException { - return new HttpEngine(this, method, requestHeaders, connection, requestBody); - } - - /** - * Aggressively tries to get the final HTTP response, potentially making - * many HTTP requests in the process in order to cope with redirects and - * authentication. - */ - private HttpEngine getResponse() throws IOException { - initHttpEngine(); - - if (httpEngine.hasResponse()) { - return httpEngine; - } - - while (true) { - if (!execute(true)) { - continue; - } - - Retry retry = processResponseHeaders(); - if (retry == Retry.NONE) { - httpEngine.automaticallyReleaseConnectionToPool(); - return httpEngine; - } + Retry retry = processResponseHeaders(); + if (retry == Retry.NONE) { + httpEngine.automaticallyReleaseConnectionToPool(); + return httpEngine; + } /* * The first request was insufficient. Prepare for another... */ - String retryMethod = method; - OutputStream requestBody = httpEngine.getRequestBody(); + String retryMethod = method; + OutputStream requestBody = httpEngine.getRequestBody(); /* * Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM * redirect should keep the same method, Chrome, Firefox and the * RI all issue GETs when following any redirect. */ - int responseCode = getResponseCode(); - if (responseCode == HTTP_MULT_CHOICE || responseCode == HTTP_MOVED_PERM - || responseCode == HTTP_MOVED_TEMP || responseCode == HTTP_SEE_OTHER) { - retryMethod = "GET"; - requestBody = null; - } + int responseCode = getResponseCode(); + if (responseCode == HTTP_MULT_CHOICE + || responseCode == HTTP_MOVED_PERM + || responseCode == HTTP_MOVED_TEMP + || responseCode == HTTP_SEE_OTHER) { + retryMethod = "GET"; + requestBody = null; + } - if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) { - throw new HttpRetryException("Cannot retry streamed HTTP body", - httpEngine.getResponseCode()); - } + if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) { + throw new HttpRetryException("Cannot retry streamed HTTP body", + httpEngine.getResponseCode()); + } - if (retry == Retry.DIFFERENT_CONNECTION) { - httpEngine.automaticallyReleaseConnectionToPool(); - } + if (retry == Retry.DIFFERENT_CONNECTION) { + httpEngine.automaticallyReleaseConnectionToPool(); + } - httpEngine.release(false); + httpEngine.release(false); - httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, - httpEngine.getConnection(), (RetryableOutputStream) requestBody); + httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(), + (RetryableOutputStream) requestBody); + } + } + + /** + * Sends a request and optionally reads a response. Returns true if the + * request was successfully executed, and false if the request can be + * retried. Throws an exception if the request failed permanently. + */ + private boolean execute(boolean readResponse) throws IOException { + try { + httpEngine.sendRequest(); + if (readResponse) { + httpEngine.readResponse(); + } + return true; + } catch (IOException e) { + RouteSelector routeSelector = httpEngine.routeSelector; + if (routeSelector != null && httpEngine.connection != null) { + routeSelector.connectFailed(httpEngine.connection, e); + } + if (routeSelector == null && httpEngine.connection == null) { + throw e; // If we failed before finding a route or a connection, give up. + } + + // The connection failure isn't fatal if there's another route to attempt. + OutputStream requestBody = httpEngine.getRequestBody(); + if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) && (requestBody + == null || requestBody instanceof RetryableOutputStream)) { + httpEngine.release(true); + httpEngine = + newHttpEngine(method, rawRequestHeaders, null, (RetryableOutputStream) requestBody); + httpEngine.routeSelector = routeSelector; // Keep the same routeSelector. + return false; + } + httpEngineFailure = e; + throw e; + } + } + + private boolean isRecoverable(IOException e) { + // If the problem was a CertificateException from the X509TrustManager, + // do not retry, we didn't have an abrupt server initiated exception. + boolean sslFailure = + e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException; + boolean protocolFailure = e instanceof ProtocolException; + return !sslFailure && !protocolFailure; + } + + HttpEngine getHttpEngine() { + return httpEngine; + } + + enum Retry { + NONE, + SAME_CONNECTION, + DIFFERENT_CONNECTION + } + + /** + * Returns the retry action to take for the current response headers. The + * headers, proxy and target URL or this connection may be adjusted to + * prepare for a follow up request. + */ + private Retry processResponseHeaders() throws IOException { + switch (getResponseCode()) { + case HTTP_PROXY_AUTH: + if (!usingProxy()) { + throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); } - } + // fall-through + case HTTP_UNAUTHORIZED: + boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(), + httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, proxy, url); + return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; - /** - * Sends a request and optionally reads a response. Returns true if the - * request was successfully executed, and false if the request can be - * retried. Throws an exception if the request failed permanently. - */ - private boolean execute(boolean readResponse) throws IOException { - try { - httpEngine.sendRequest(); - if (readResponse) { - httpEngine.readResponse(); - } - return true; - } catch (IOException e) { - RouteSelector routeSelector = httpEngine.routeSelector; - if (routeSelector != null && httpEngine.connection != null) { - routeSelector.connectFailed(httpEngine.connection, e); - } - if (routeSelector == null && httpEngine.connection == null) { - throw e; // If we failed before finding a route or a connection, give up. - } - - // The connection failure isn't fatal if there's another route to attempt. - OutputStream requestBody = httpEngine.getRequestBody(); - if ((routeSelector == null || routeSelector.hasNext()) && isRecoverable(e) - && (requestBody == null || requestBody instanceof RetryableOutputStream)) { - httpEngine.release(true); - httpEngine = newHttpEngine(method, rawRequestHeaders, null, - (RetryableOutputStream) requestBody); - httpEngine.routeSelector = routeSelector; // Keep the same routeSelector. - return false; - } - httpEngineFailure = e; - throw e; + case HTTP_MULT_CHOICE: + case HTTP_MOVED_PERM: + case HTTP_MOVED_TEMP: + case HTTP_SEE_OTHER: + if (!getInstanceFollowRedirects()) { + return Retry.NONE; } - } - - private boolean isRecoverable(IOException e) { - // If the problem was a CertificateException from the X509TrustManager, - // do not retry, we didn't have an abrupt server initiated exception. - boolean sslFailure = e instanceof SSLHandshakeException - && e.getCause() instanceof CertificateException; - boolean protocolFailure = e instanceof ProtocolException; - return !sslFailure && !protocolFailure; - } - - HttpEngine getHttpEngine() { - return httpEngine; - } - - enum Retry { - NONE, - SAME_CONNECTION, - DIFFERENT_CONNECTION - } - - /** - * Returns the retry action to take for the current response headers. The - * headers, proxy and target URL or this connection may be adjusted to - * prepare for a follow up request. - */ - private Retry processResponseHeaders() throws IOException { - switch (getResponseCode()) { - case HTTP_PROXY_AUTH: - if (!usingProxy()) { - throw new ProtocolException( - "Received HTTP_PROXY_AUTH (407) code while not using proxy"); - } - // fall-through - case HTTP_UNAUTHORIZED: - boolean credentialsFound = HttpAuthenticator.processAuthHeader(getResponseCode(), - httpEngine.getResponseHeaders().getHeaders(), rawRequestHeaders, proxy, url); - return credentialsFound ? Retry.SAME_CONNECTION : Retry.NONE; - - case HTTP_MULT_CHOICE: - case HTTP_MOVED_PERM: - case HTTP_MOVED_TEMP: - case HTTP_SEE_OTHER: - if (!getInstanceFollowRedirects()) { - return Retry.NONE; - } - if (++redirectionCount > MAX_REDIRECTS) { - throw new ProtocolException("Too many redirects: " + redirectionCount); - } - String location = getHeaderField("Location"); - if (location == null) { - return Retry.NONE; - } - URL previousUrl = url; - url = new URL(previousUrl, location); - if (!previousUrl.getProtocol().equals(url.getProtocol())) { - return Retry.NONE; // the scheme changed; don't retry. - } - if (previousUrl.getHost().equals(url.getHost()) - && getEffectivePort(previousUrl) == getEffectivePort(url)) { - return Retry.SAME_CONNECTION; - } else { - return Retry.DIFFERENT_CONNECTION; - } - - default: - return Retry.NONE; + if (++redirectionCount > MAX_REDIRECTS) { + throw new ProtocolException("Too many redirects: " + redirectionCount); } - } - - final int getDefaultPort() { - return defaultPort; - } - - /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */ - final int getFixedContentLength() { - return fixedContentLength; - } - - /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */ - final int getChunkLength() { - return chunkLength; - } - - final Proxy getProxy() { - return proxy; - } - - final void setProxy(Proxy proxy) { - this.proxy = proxy; - } - - @Override public final boolean usingProxy() { - return (proxy != null && proxy.type() != Proxy.Type.DIRECT); - } - - @Override public String getResponseMessage() throws IOException { - return getResponse().getResponseHeaders().getHeaders().getResponseMessage(); - } - - @Override public final int getResponseCode() throws IOException { - return getResponse().getResponseCode(); - } - - @Override public final void setRequestProperty(String field, String newValue) { - if (connected) { - throw new IllegalStateException("Cannot set request property after connection is made"); + String location = getHeaderField("Location"); + if (location == null) { + return Retry.NONE; } - if (field == null) { - throw new NullPointerException("field == null"); + URL previousUrl = url; + url = new URL(previousUrl, location); + if (!previousUrl.getProtocol().equals(url.getProtocol())) { + return Retry.NONE; // the scheme changed; don't retry. + } + if (previousUrl.getHost().equals(url.getHost()) + && getEffectivePort(previousUrl) == getEffectivePort(url)) { + return Retry.SAME_CONNECTION; + } else { + return Retry.DIFFERENT_CONNECTION; } - rawRequestHeaders.set(field, newValue); - } - @Override public final void addRequestProperty(String field, String value) { - if (connected) { - throw new IllegalStateException("Cannot add request property after connection is made"); - } - if (field == null) { - throw new NullPointerException("field == null"); - } - rawRequestHeaders.add(field, value); + default: + return Retry.NONE; } + } + + final int getDefaultPort() { + return defaultPort; + } + + /** @see java.net.HttpURLConnection#setFixedLengthStreamingMode(int) */ + final int getFixedContentLength() { + return fixedContentLength; + } + + /** @see java.net.HttpURLConnection#setChunkedStreamingMode(int) */ + final int getChunkLength() { + return chunkLength; + } + + final Proxy getProxy() { + return proxy; + } + + final void setProxy(Proxy proxy) { + this.proxy = proxy; + } + + @Override public final boolean usingProxy() { + return (proxy != null && proxy.type() != Proxy.Type.DIRECT); + } + + @Override public String getResponseMessage() throws IOException { + return getResponse().getResponseHeaders().getHeaders().getResponseMessage(); + } + + @Override public final int getResponseCode() throws IOException { + return getResponse().getResponseCode(); + } + + @Override public final void setRequestProperty(String field, String newValue) { + if (connected) { + throw new IllegalStateException("Cannot set request property after connection is made"); + } + if (field == null) { + throw new NullPointerException("field == null"); + } + rawRequestHeaders.set(field, newValue); + } + + @Override public final void addRequestProperty(String field, String value) { + if (connected) { + throw new IllegalStateException("Cannot add request property after connection is made"); + } + if (field == null) { + throw new NullPointerException("field == null"); + } + rawRequestHeaders.add(field, value); + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java index b0fc73b4d..821290311 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java +++ b/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java @@ -19,7 +19,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.TunnelRequest; -import static com.squareup.okhttp.internal.Util.getEffectivePort; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -43,418 +42,418 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; +import static com.squareup.okhttp.internal.Util.getEffectivePort; + public final class HttpsURLConnectionImpl extends HttpsURLConnection { - /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */ - private final HttpUrlConnectionDelegate delegate; + /** HttpUrlConnectionDelegate allows reuse of HttpURLConnectionImpl. */ + private final HttpUrlConnectionDelegate delegate; - public HttpsURLConnectionImpl(URL url, int defaultPort, Proxy proxy, - ProxySelector proxySelector, CookieHandler cookieHandler, ResponseCache responseCache, - ConnectionPool connectionPool) { - super(url); - delegate = new HttpUrlConnectionDelegate(url, defaultPort, proxy, proxySelector, - cookieHandler, responseCache, connectionPool); + public HttpsURLConnectionImpl(URL url, int defaultPort, Proxy proxy, ProxySelector proxySelector, + CookieHandler cookieHandler, ResponseCache responseCache, ConnectionPool connectionPool) { + super(url); + delegate = new HttpUrlConnectionDelegate(url, defaultPort, proxy, proxySelector, cookieHandler, + responseCache, connectionPool); + } + + private void checkConnected() { + if (delegate.getSSLSocket() == null) { + throw new IllegalStateException("Connection has not yet been established"); + } + } + + HttpEngine getHttpEngine() { + return delegate.getHttpEngine(); + } + + @Override + public String getCipherSuite() { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + return cacheResponse.getCipherSuite(); + } + checkConnected(); + return delegate.getSSLSocket().getSession().getCipherSuite(); + } + + @Override + public Certificate[] getLocalCertificates() { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + List result = cacheResponse.getLocalCertificateChain(); + return result != null ? result.toArray(new Certificate[result.size()]) : null; + } + checkConnected(); + return delegate.getSSLSocket().getSession().getLocalCertificates(); + } + + @Override + public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + List result = cacheResponse.getServerCertificateChain(); + return result != null ? result.toArray(new Certificate[result.size()]) : null; + } + checkConnected(); + return delegate.getSSLSocket().getSession().getPeerCertificates(); + } + + @Override + public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + return cacheResponse.getPeerPrincipal(); + } + checkConnected(); + return delegate.getSSLSocket().getSession().getPeerPrincipal(); + } + + @Override + public Principal getLocalPrincipal() { + SecureCacheResponse cacheResponse = delegate.getCacheResponse(); + if (cacheResponse != null) { + return cacheResponse.getLocalPrincipal(); + } + checkConnected(); + return delegate.getSSLSocket().getSession().getLocalPrincipal(); + } + + @Override + public void disconnect() { + delegate.disconnect(); + } + + @Override + public InputStream getErrorStream() { + return delegate.getErrorStream(); + } + + @Override + public String getRequestMethod() { + return delegate.getRequestMethod(); + } + + @Override + public int getResponseCode() throws IOException { + return delegate.getResponseCode(); + } + + @Override + public String getResponseMessage() throws IOException { + return delegate.getResponseMessage(); + } + + @Override + public void setRequestMethod(String method) throws ProtocolException { + delegate.setRequestMethod(method); + } + + @Override + public boolean usingProxy() { + return delegate.usingProxy(); + } + + @Override + public boolean getInstanceFollowRedirects() { + return delegate.getInstanceFollowRedirects(); + } + + @Override + public void setInstanceFollowRedirects(boolean followRedirects) { + delegate.setInstanceFollowRedirects(followRedirects); + } + + @Override + public void connect() throws IOException { + connected = true; + delegate.connect(); + } + + @Override + public boolean getAllowUserInteraction() { + return delegate.getAllowUserInteraction(); + } + + @Override + public Object getContent() throws IOException { + return delegate.getContent(); + } + + @SuppressWarnings("unchecked") // Spec does not generify + @Override + public Object getContent(Class[] types) throws IOException { + return delegate.getContent(types); + } + + @Override + public String getContentEncoding() { + return delegate.getContentEncoding(); + } + + @Override + public int getContentLength() { + return delegate.getContentLength(); + } + + @Override + public String getContentType() { + return delegate.getContentType(); + } + + @Override + public long getDate() { + return delegate.getDate(); + } + + @Override + public boolean getDefaultUseCaches() { + return delegate.getDefaultUseCaches(); + } + + @Override + public boolean getDoInput() { + return delegate.getDoInput(); + } + + @Override + public boolean getDoOutput() { + return delegate.getDoOutput(); + } + + @Override + public long getExpiration() { + return delegate.getExpiration(); + } + + @Override + public String getHeaderField(int pos) { + return delegate.getHeaderField(pos); + } + + @Override + public Map> getHeaderFields() { + return delegate.getHeaderFields(); + } + + @Override + public Map> getRequestProperties() { + return delegate.getRequestProperties(); + } + + @Override + public void addRequestProperty(String field, String newValue) { + delegate.addRequestProperty(field, newValue); + } + + @Override + public String getHeaderField(String key) { + return delegate.getHeaderField(key); + } + + @Override + public long getHeaderFieldDate(String field, long defaultValue) { + return delegate.getHeaderFieldDate(field, defaultValue); + } + + @Override + public int getHeaderFieldInt(String field, int defaultValue) { + return delegate.getHeaderFieldInt(field, defaultValue); + } + + @Override + public String getHeaderFieldKey(int position) { + return delegate.getHeaderFieldKey(position); + } + + @Override + public long getIfModifiedSince() { + return delegate.getIfModifiedSince(); + } + + @Override + public InputStream getInputStream() throws IOException { + return delegate.getInputStream(); + } + + @Override + public long getLastModified() { + return delegate.getLastModified(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return delegate.getOutputStream(); + } + + @Override + public Permission getPermission() throws IOException { + return delegate.getPermission(); + } + + @Override + public String getRequestProperty(String field) { + return delegate.getRequestProperty(field); + } + + @Override + public URL getURL() { + return delegate.getURL(); + } + + @Override + public boolean getUseCaches() { + return delegate.getUseCaches(); + } + + @Override + public void setAllowUserInteraction(boolean newValue) { + delegate.setAllowUserInteraction(newValue); + } + + @Override + public void setDefaultUseCaches(boolean newValue) { + delegate.setDefaultUseCaches(newValue); + } + + @Override + public void setDoInput(boolean newValue) { + delegate.setDoInput(newValue); + } + + @Override + public void setDoOutput(boolean newValue) { + delegate.setDoOutput(newValue); + } + + @Override + public void setIfModifiedSince(long newValue) { + delegate.setIfModifiedSince(newValue); + } + + @Override + public void setRequestProperty(String field, String newValue) { + delegate.setRequestProperty(field, newValue); + } + + @Override + public void setUseCaches(boolean newValue) { + delegate.setUseCaches(newValue); + } + + @Override + public void setConnectTimeout(int timeoutMillis) { + delegate.setConnectTimeout(timeoutMillis); + } + + @Override + public int getConnectTimeout() { + return delegate.getConnectTimeout(); + } + + @Override + public void setReadTimeout(int timeoutMillis) { + delegate.setReadTimeout(timeoutMillis); + } + + @Override + public int getReadTimeout() { + return delegate.getReadTimeout(); + } + + @Override + public String toString() { + return delegate.toString(); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + delegate.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setChunkedStreamingMode(int chunkLength) { + delegate.setChunkedStreamingMode(chunkLength); + } + + private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { + private HttpUrlConnectionDelegate(URL url, int defaultPort, Proxy proxy, + ProxySelector proxySelector, CookieHandler cookieHandler, ResponseCache responseCache, + ConnectionPool connectionPool) { + super(url, defaultPort, proxy, proxySelector, cookieHandler, responseCache, connectionPool); } - private void checkConnected() { - if (delegate.getSSLSocket() == null) { - throw new IllegalStateException("Connection has not yet been established"); - } + @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, + Connection connection, RetryableOutputStream requestBody) throws IOException { + return new HttpsEngine(this, method, requestHeaders, connection, requestBody, + HttpsURLConnectionImpl.this); } - HttpEngine getHttpEngine() { - return delegate.getHttpEngine(); + public SecureCacheResponse getCacheResponse() { + HttpsEngine engine = (HttpsEngine) httpEngine; + return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null; } - @Override - public String getCipherSuite() { - SecureCacheResponse cacheResponse = delegate.getCacheResponse(); - if (cacheResponse != null) { - return cacheResponse.getCipherSuite(); - } - checkConnected(); - return delegate.getSSLSocket().getSession().getCipherSuite(); + public SSLSocket getSSLSocket() { + HttpsEngine engine = (HttpsEngine) httpEngine; + return engine != null ? engine.sslSocket : null; + } + } + + private static final class HttpsEngine extends HttpEngine { + /** + * Stash of HttpsEngine.connection.socket to implement requests like + * {@link #getCipherSuite} even after the connection has been recycled. + */ + private SSLSocket sslSocket; + + private final HttpsURLConnectionImpl enclosing; + + /** + * @param policy the HttpURLConnectionImpl with connection configuration + * @param enclosing the HttpsURLConnection with HTTPS features + */ + private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, + Connection connection, RetryableOutputStream requestBody, HttpsURLConnectionImpl enclosing) + throws IOException { + super(policy, method, requestHeaders, connection, requestBody); + this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null; + this.enclosing = enclosing; } - @Override - public Certificate[] getLocalCertificates() { - SecureCacheResponse cacheResponse = delegate.getCacheResponse(); - if (cacheResponse != null) { - List result = cacheResponse.getLocalCertificateChain(); - return result != null ? result.toArray(new Certificate[result.size()]) : null; - } - checkConnected(); - return delegate.getSSLSocket().getSession().getLocalCertificates(); + @Override protected void connected(Connection connection) { + this.sslSocket = (SSLSocket) connection.getSocket(); } - @Override - public Certificate[] getServerCertificates() throws SSLPeerUnverifiedException { - SecureCacheResponse cacheResponse = delegate.getCacheResponse(); - if (cacheResponse != null) { - List result = cacheResponse.getServerCertificateChain(); - return result != null ? result.toArray(new Certificate[result.size()]) : null; - } - checkConnected(); - return delegate.getSSLSocket().getSession().getPeerCertificates(); + @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { + return cacheResponse instanceof SecureCacheResponse; } - @Override - public Principal getPeerPrincipal() throws SSLPeerUnverifiedException { - SecureCacheResponse cacheResponse = delegate.getCacheResponse(); - if (cacheResponse != null) { - return cacheResponse.getPeerPrincipal(); - } - checkConnected(); - return delegate.getSSLSocket().getSession().getPeerPrincipal(); + @Override protected boolean includeAuthorityInRequestLine() { + // Even if there is a proxy, it isn't involved. Always request just the file. + return false; } - @Override - public Principal getLocalPrincipal() { - SecureCacheResponse cacheResponse = delegate.getCacheResponse(); - if (cacheResponse != null) { - return cacheResponse.getLocalPrincipal(); - } - checkConnected(); - return delegate.getSSLSocket().getSession().getLocalPrincipal(); + @Override protected SSLSocketFactory getSslSocketFactory() { + return enclosing.getSSLSocketFactory(); } - @Override - public void disconnect() { - delegate.disconnect(); + @Override protected HostnameVerifier getHostnameVerifier() { + return enclosing.getHostnameVerifier(); } - @Override - public InputStream getErrorStream() { - return delegate.getErrorStream(); + @Override protected HttpURLConnection getHttpConnectionToCache() { + return enclosing; } - @Override - public String getRequestMethod() { - return delegate.getRequestMethod(); - } - - @Override - public int getResponseCode() throws IOException { - return delegate.getResponseCode(); - } - - @Override - public String getResponseMessage() throws IOException { - return delegate.getResponseMessage(); - } - - @Override - public void setRequestMethod(String method) throws ProtocolException { - delegate.setRequestMethod(method); - } - - @Override - public boolean usingProxy() { - return delegate.usingProxy(); - } - - @Override - public boolean getInstanceFollowRedirects() { - return delegate.getInstanceFollowRedirects(); - } - - @Override - public void setInstanceFollowRedirects(boolean followRedirects) { - delegate.setInstanceFollowRedirects(followRedirects); - } - - @Override - public void connect() throws IOException { - connected = true; - delegate.connect(); - } - - @Override - public boolean getAllowUserInteraction() { - return delegate.getAllowUserInteraction(); - } - - @Override - public Object getContent() throws IOException { - return delegate.getContent(); - } - - @SuppressWarnings("unchecked") // Spec does not generify - @Override - public Object getContent(Class[] types) throws IOException { - return delegate.getContent(types); - } - - @Override - public String getContentEncoding() { - return delegate.getContentEncoding(); - } - - @Override - public int getContentLength() { - return delegate.getContentLength(); - } - - @Override - public String getContentType() { - return delegate.getContentType(); - } - - @Override - public long getDate() { - return delegate.getDate(); - } - - @Override - public boolean getDefaultUseCaches() { - return delegate.getDefaultUseCaches(); - } - - @Override - public boolean getDoInput() { - return delegate.getDoInput(); - } - - @Override - public boolean getDoOutput() { - return delegate.getDoOutput(); - } - - @Override - public long getExpiration() { - return delegate.getExpiration(); - } - - @Override - public String getHeaderField(int pos) { - return delegate.getHeaderField(pos); - } - - @Override - public Map> getHeaderFields() { - return delegate.getHeaderFields(); - } - - @Override - public Map> getRequestProperties() { - return delegate.getRequestProperties(); - } - - @Override - public void addRequestProperty(String field, String newValue) { - delegate.addRequestProperty(field, newValue); - } - - @Override - public String getHeaderField(String key) { - return delegate.getHeaderField(key); - } - - @Override - public long getHeaderFieldDate(String field, long defaultValue) { - return delegate.getHeaderFieldDate(field, defaultValue); - } - - @Override - public int getHeaderFieldInt(String field, int defaultValue) { - return delegate.getHeaderFieldInt(field, defaultValue); - } - - @Override - public String getHeaderFieldKey(int position) { - return delegate.getHeaderFieldKey(position); - } - - @Override - public long getIfModifiedSince() { - return delegate.getIfModifiedSince(); - } - - @Override - public InputStream getInputStream() throws IOException { - return delegate.getInputStream(); - } - - @Override - public long getLastModified() { - return delegate.getLastModified(); - } - - @Override - public OutputStream getOutputStream() throws IOException { - return delegate.getOutputStream(); - } - - @Override - public Permission getPermission() throws IOException { - return delegate.getPermission(); - } - - @Override - public String getRequestProperty(String field) { - return delegate.getRequestProperty(field); - } - - @Override - public URL getURL() { - return delegate.getURL(); - } - - @Override - public boolean getUseCaches() { - return delegate.getUseCaches(); - } - - @Override - public void setAllowUserInteraction(boolean newValue) { - delegate.setAllowUserInteraction(newValue); - } - - @Override - public void setDefaultUseCaches(boolean newValue) { - delegate.setDefaultUseCaches(newValue); - } - - @Override - public void setDoInput(boolean newValue) { - delegate.setDoInput(newValue); - } - - @Override - public void setDoOutput(boolean newValue) { - delegate.setDoOutput(newValue); - } - - @Override - public void setIfModifiedSince(long newValue) { - delegate.setIfModifiedSince(newValue); - } - - @Override - public void setRequestProperty(String field, String newValue) { - delegate.setRequestProperty(field, newValue); - } - - @Override - public void setUseCaches(boolean newValue) { - delegate.setUseCaches(newValue); - } - - @Override - public void setConnectTimeout(int timeoutMillis) { - delegate.setConnectTimeout(timeoutMillis); - } - - @Override - public int getConnectTimeout() { - return delegate.getConnectTimeout(); - } - - @Override - public void setReadTimeout(int timeoutMillis) { - delegate.setReadTimeout(timeoutMillis); - } - - @Override - public int getReadTimeout() { - return delegate.getReadTimeout(); - } - - @Override - public String toString() { - return delegate.toString(); - } - - @Override - public void setFixedLengthStreamingMode(int contentLength) { - delegate.setFixedLengthStreamingMode(contentLength); - } - - @Override - public void setChunkedStreamingMode(int chunkLength) { - delegate.setChunkedStreamingMode(chunkLength); - } - - private final class HttpUrlConnectionDelegate extends HttpURLConnectionImpl { - private HttpUrlConnectionDelegate(URL url, int defaultPort, Proxy proxy, - ProxySelector proxySelector, CookieHandler cookieHandler, - ResponseCache responseCache, ConnectionPool connectionPool) { - super(url, defaultPort, proxy, proxySelector, cookieHandler, responseCache, - connectionPool); - } - - @Override protected HttpEngine newHttpEngine(String method, RawHeaders requestHeaders, - Connection connection, RetryableOutputStream requestBody) throws IOException { - return new HttpsEngine(this, method, requestHeaders, connection, requestBody, - HttpsURLConnectionImpl.this); - } - - public SecureCacheResponse getCacheResponse() { - HttpsEngine engine = (HttpsEngine) httpEngine; - return engine != null ? (SecureCacheResponse) engine.getCacheResponse() : null; - } - - public SSLSocket getSSLSocket() { - HttpsEngine engine = (HttpsEngine) httpEngine; - return engine != null ? engine.sslSocket : null; - } - } - - private static final class HttpsEngine extends HttpEngine { - /** - * Stash of HttpsEngine.connection.socket to implement requests like - * {@link #getCipherSuite} even after the connection has been recycled. - */ - private SSLSocket sslSocket; - - private final HttpsURLConnectionImpl enclosing; - - /** - * @param policy the HttpURLConnectionImpl with connection configuration - * @param enclosing the HttpsURLConnection with HTTPS features - */ - private HttpsEngine(HttpURLConnectionImpl policy, String method, RawHeaders requestHeaders, - Connection connection, RetryableOutputStream requestBody, - HttpsURLConnectionImpl enclosing) throws IOException { - super(policy, method, requestHeaders, connection, requestBody); - this.sslSocket = connection != null ? (SSLSocket) connection.getSocket() : null; - this.enclosing = enclosing; - } - - @Override protected void connected(Connection connection) { - this.sslSocket = (SSLSocket) connection.getSocket(); - } - - @Override protected boolean acceptCacheResponseType(CacheResponse cacheResponse) { - return cacheResponse instanceof SecureCacheResponse; - } - - @Override protected boolean includeAuthorityInRequestLine() { - // Even if there is a proxy, it isn't involved. Always request just the file. - return false; - } - - @Override protected SSLSocketFactory getSslSocketFactory() { - return enclosing.getSSLSocketFactory(); - } - - @Override protected HostnameVerifier getHostnameVerifier() { - return enclosing.getHostnameVerifier(); - } - - @Override protected HttpURLConnection getHttpConnectionToCache() { - return enclosing; - } - - @Override protected TunnelRequest getTunnelConfig() { - String userAgent = requestHeaders.getUserAgent(); - if (userAgent == null) { - userAgent = getDefaultUserAgent(); - } - - URL url = policy.getURL(); - return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent, - requestHeaders.getProxyAuthorization()); - } + @Override protected TunnelRequest getTunnelConfig() { + String userAgent = requestHeaders.getUserAgent(); + if (userAgent == null) { + userAgent = getDefaultUserAgent(); + } + + URL url = policy.getURL(); + return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent, + requestHeaders.getProxyAuthorization()); } + } } 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 8b17a840d..52dbb714f 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java +++ b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java @@ -51,393 +51,374 @@ import java.util.TreeMap; * leading or trailing whitespace. */ public final class RawHeaders { - private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { - // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") - @Override public int compare(String a, String b) { - if (a == b) { - return 0; - } else if (a == null) { - return -1; - } else if (b == null) { - return 1; - } else { - return String.CASE_INSENSITIVE_ORDER.compare(a, b); - } - } - }; - - private final List namesAndValues = new ArrayList(20); - private String requestLine; - private String statusLine; - private int httpMinorVersion = 1; - private int responseCode = -1; - private String responseMessage; - - public RawHeaders() { + private static final Comparator FIELD_NAME_COMPARATOR = new Comparator() { + // @FindBugsSuppressWarnings("ES_COMPARING_PARAMETER_STRING_WITH_EQ") + @Override public int compare(String a, String b) { + if (a == b) { + return 0; + } else if (a == null) { + return -1; + } else if (b == null) { + return 1; + } else { + return String.CASE_INSENSITIVE_ORDER.compare(a, b); + } } + }; - public RawHeaders(RawHeaders copyFrom) { - namesAndValues.addAll(copyFrom.namesAndValues); - requestLine = copyFrom.requestLine; - statusLine = copyFrom.statusLine; - httpMinorVersion = copyFrom.httpMinorVersion; - responseCode = copyFrom.responseCode; - responseMessage = copyFrom.responseMessage; + private final List namesAndValues = new ArrayList(20); + private String requestLine; + private String statusLine; + private int httpMinorVersion = 1; + private int responseCode = -1; + private String responseMessage; + + public RawHeaders() { + } + + public RawHeaders(RawHeaders copyFrom) { + namesAndValues.addAll(copyFrom.namesAndValues); + requestLine = copyFrom.requestLine; + statusLine = copyFrom.statusLine; + httpMinorVersion = copyFrom.httpMinorVersion; + responseCode = copyFrom.responseCode; + responseMessage = copyFrom.responseMessage; + } + + /** Sets the request line (like "GET / HTTP/1.1"). */ + public void setRequestLine(String requestLine) { + requestLine = requestLine.trim(); + this.requestLine = requestLine; + } + + /** Sets the response status line (like "HTTP/1.0 200 OK"). */ + public void setStatusLine(String statusLine) throws IOException { + // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 + if (!statusLine.startsWith("HTTP/1.") + || statusLine.charAt(8) != ' ' + || statusLine.charAt(12) != ' ') { + throw new ProtocolException("Unexpected status line: " + statusLine); } - - /** - * Sets the request line (like "GET / HTTP/1.1"). - */ - public void setRequestLine(String requestLine) { - requestLine = requestLine.trim(); - this.requestLine = requestLine; + int httpMinorVersion = statusLine.charAt(7) - '0'; + if (httpMinorVersion < 0 || httpMinorVersion > 9) { + throw new ProtocolException("Unexpected status line: " + statusLine); } - - /** - * Sets the response status line (like "HTTP/1.0 200 OK"). - */ - public void setStatusLine(String statusLine) throws IOException { - // H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t - // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 - if (!statusLine.startsWith("HTTP/1.") - || statusLine.charAt(8) != ' ' - || statusLine.charAt(12) != ' ') { - throw new ProtocolException("Unexpected status line: " + statusLine); - } - int httpMinorVersion = statusLine.charAt(7) - '0'; - if (httpMinorVersion < 0 || httpMinorVersion > 9) { - throw new ProtocolException("Unexpected status line: " + statusLine); - } - int responseCode; - try { - responseCode = Integer.parseInt(statusLine.substring(9, 12)); - } catch (NumberFormatException e) { - throw new ProtocolException("Unexpected status line: " + statusLine); - } - this.responseMessage = statusLine.substring(13); - this.responseCode = responseCode; - this.statusLine = statusLine; - this.httpMinorVersion = httpMinorVersion; + int responseCode; + try { + responseCode = Integer.parseInt(statusLine.substring(9, 12)); + } catch (NumberFormatException e) { + throw new ProtocolException("Unexpected status line: " + statusLine); } + this.responseMessage = statusLine.substring(13); + this.responseCode = responseCode; + this.statusLine = statusLine; + this.httpMinorVersion = httpMinorVersion; + } - public void computeResponseStatusLineFromSpdyHeaders() throws IOException { - String status = null; - String version = null; - for (int i = 0; i < namesAndValues.size(); i += 2) { - String name = namesAndValues.get(i); - if (":status".equals(name)) { - status = namesAndValues.get(i + 1); - } 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"); - } - setStatusLine(version + " " + status); + public void computeResponseStatusLineFromSpdyHeaders() throws IOException { + String status = null; + String version = null; + for (int i = 0; i < namesAndValues.size(); i += 2) { + String name = namesAndValues.get(i); + if (":status".equals(name)) { + status = namesAndValues.get(i + 1); + } else if (":version".equals(name)) { + version = namesAndValues.get(i + 1); + } } - - /** - * @param method like "GET", "POST", "HEAD", etc. - * @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 path, String version, String host, String scheme) { - // TODO: populate the statusLine for the client's benefit? - add(":method", method); - add(":scheme", scheme); - add(":path", path); - add(":version", version); - add(":host", host); + if (status == null || version == null) { + throw new ProtocolException("Expected ':status' and ':version' headers not present"); } + setStatusLine(version + " " + status); + } - public String getStatusLine() { - return statusLine; + /** + * @param method like "GET", "POST", "HEAD", etc. + * @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 path, String version, String host, + String scheme) { + // TODO: populate the statusLine for the client's benefit? + add(":method", method); + add(":scheme", scheme); + add(":path", path); + add(":version", version); + add(":host", host); + } + + public String getStatusLine() { + return statusLine; + } + + /** + * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 + * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. + */ + public int getHttpMinorVersion() { + return httpMinorVersion != -1 ? httpMinorVersion : 1; + } + + /** Returns the HTTP status code or -1 if it is unknown. */ + public int getResponseCode() { + return responseCode; + } + + /** Returns the HTTP status message or null if it is unknown. */ + public String getResponseMessage() { + return responseMessage; + } + + /** + * Add an HTTP header line containing a field name, a literal colon, and a + * value. + */ + public void addLine(String line) { + int index = line.indexOf(":"); + if (index == -1) { + add("", line); + } else { + add(line.substring(0, index), line.substring(index + 1)); } + } - /** - * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0 - * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown. - */ - public int getHttpMinorVersion() { - return httpMinorVersion != -1 ? httpMinorVersion : 1; + /** Add a field with the specified value. */ + public void add(String fieldName, String value) { + if (fieldName == null) { + throw new IllegalArgumentException("fieldName == null"); } - - /** - * Returns the HTTP status code or -1 if it is unknown. - */ - public int getResponseCode() { - return responseCode; - } - - /** - * Returns the HTTP status message or null if it is unknown. - */ - public String getResponseMessage() { - return responseMessage; - } - - /** - * Add an HTTP header line containing a field name, a literal colon, and a - * value. - */ - public void addLine(String line) { - int index = line.indexOf(":"); - if (index == -1) { - add("", line); - } else { - add(line.substring(0, index), line.substring(index + 1)); - } - } - - /** - * Add a field with the specified value. - */ - public void add(String fieldName, String value) { - if (fieldName == null) { - throw new IllegalArgumentException("fieldName == null"); - } - if (value == null) { + if (value == null) { /* * Given null values, the RI sends a malformed field line like * "Accept\r\n". For platform compatibility and HTTP compliance, we * print a warning and ignore null values. */ - Platform.get().logW("Ignoring HTTP header field '" - + fieldName + "' because its value is null"); - return; + Platform.get() + .logW("Ignoring HTTP header field '" + fieldName + "' because its value is null"); + return; + } + namesAndValues.add(fieldName); + namesAndValues.add(value.trim()); + } + + public void removeAll(String fieldName) { + for (int i = 0; i < namesAndValues.size(); i += 2) { + if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { + namesAndValues.remove(i); // field name + namesAndValues.remove(i); // value + } + } + } + + public void addAll(String fieldName, List headerFields) { + for (String value : headerFields) { + add(fieldName, value); + } + } + + /** + * Set a field with the specified value. If the field is not found, it is + * added. If the field is found, the existing values are replaced. + */ + public void set(String fieldName, String value) { + removeAll(fieldName); + add(fieldName, value); + } + + /** Returns the number of field values. */ + public int length() { + return namesAndValues.size() / 2; + } + + /** Returns the field at {@code position} or null if that is out of range. */ + public String getFieldName(int index) { + int fieldNameIndex = index * 2; + if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) { + return null; + } + return namesAndValues.get(fieldNameIndex); + } + + /** Returns the value at {@code index} or null if that is out of range. */ + public String getValue(int index) { + int valueIndex = index * 2 + 1; + if (valueIndex < 0 || valueIndex >= namesAndValues.size()) { + return null; + } + return namesAndValues.get(valueIndex); + } + + /** Returns the last value corresponding to the specified field, or null. */ + public String get(String fieldName) { + for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { + if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { + return namesAndValues.get(i + 1); + } + } + return null; + } + + /** @param fieldNames a case-insensitive set of HTTP header field names. */ + public RawHeaders getAll(Set fieldNames) { + RawHeaders result = new RawHeaders(); + for (int i = 0; i < namesAndValues.size(); i += 2) { + String fieldName = namesAndValues.get(i); + if (fieldNames.contains(fieldName)) { + result.add(fieldName, namesAndValues.get(i + 1)); + } + } + return result; + } + + /** Returns bytes of a request header for sending on an HTTP transport. */ + public byte[] toBytes() throws UnsupportedEncodingException { + StringBuilder result = new StringBuilder(256); + result.append(requestLine).append("\r\n"); + for (int i = 0; i < namesAndValues.size(); i += 2) { + result.append(namesAndValues.get(i)) + .append(": ") + .append(namesAndValues.get(i + 1)) + .append("\r\n"); + } + result.append("\r\n"); + return result.toString().getBytes("ISO-8859-1"); + } + + /** Parses bytes of a response header from an HTTP transport. */ + public static RawHeaders fromBytes(InputStream in) throws IOException { + RawHeaders headers; + do { + headers = new RawHeaders(); + headers.setStatusLine(Util.readAsciiLine(in)); + readHeaders(in, headers); + } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE); + return headers; + } + + /** Reads headers or trailers into {@code out}. */ + public static void readHeaders(InputStream in, RawHeaders out) throws IOException { + // parse the result headers until the first blank line + String line; + while ((line = Util.readAsciiLine(in)).length() != 0) { + out.addLine(line); + } + } + + /** + * Returns an immutable map containing each field to its list of values. The + * status line is mapped to null. + */ + public Map> toMultimap(boolean response) { + Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); + for (int i = 0; i < namesAndValues.size(); i += 2) { + String fieldName = namesAndValues.get(i); + String value = namesAndValues.get(i + 1); + + List allValues = new ArrayList(); + List otherValues = result.get(fieldName); + if (otherValues != null) { + allValues.addAll(otherValues); + } + allValues.add(value); + result.put(fieldName, Collections.unmodifiableList(allValues)); + } + if (response && statusLine != null) { + result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine))); + } else if (requestLine != null) { + result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine))); + } + return Collections.unmodifiableMap(result); + } + + /** + * Creates a new instance from the given map of fields to values. If + * present, the null field's last element will be used to set the status + * line. + */ + public static RawHeaders fromMultimap(Map> map, boolean response) + throws IOException { + if (!response) throw new UnsupportedOperationException(); + RawHeaders result = new RawHeaders(); + for (Entry> entry : map.entrySet()) { + String fieldName = entry.getKey(); + List values = entry.getValue(); + if (fieldName != null) { + result.addAll(fieldName, values); + } else if (!values.isEmpty()) { + result.setStatusLine(values.get(values.size() - 1)); + } + } + return result; + } + + /** + * Returns a list of alternating names and values. Names are all lower case. + * No names are repeated. If any name has multiple values, they are + * concatenated using "\0" as a delimiter. + */ + public List toNameValueBlock() { + Set names = new HashSet(); + List result = new ArrayList(); + for (int i = 0; i < namesAndValues.size(); i += 2) { + String name = namesAndValues.get(i).toLowerCase(Locale.US); + String value = namesAndValues.get(i + 1); + + // TODO: promote this check to where names and values are created + if (name.length() == 0 + || value.length() == 0 + || name.indexOf('\0') != -1 + || value.indexOf('\0') != -1) { + throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); + } + + // 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; + } + + // If we haven't seen this name before, add the pair to the end of the list... + if (names.add(name)) { + result.add(name); + result.add(value); + continue; + } + + // ...otherwise concatenate the existing values and this value. + for (int j = 0; j < result.size(); j += 2) { + if (name.equals(result.get(j))) { + result.set(j + 1, result.get(j + 1) + "\0" + value); + break; } - namesAndValues.add(fieldName); - namesAndValues.add(value.trim()); + } } + return result; + } - public void removeAll(String fieldName) { - for (int i = 0; i < namesAndValues.size(); i += 2) { - if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { - namesAndValues.remove(i); // field name - namesAndValues.remove(i); // value - } + public static RawHeaders fromNameValueBlock(List nameValueBlock) { + if (nameValueBlock.size() % 2 != 0) { + throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock); + } + RawHeaders result = new RawHeaders(); + for (int i = 0; i < nameValueBlock.size(); i += 2) { + String name = nameValueBlock.get(i); + String values = nameValueBlock.get(i + 1); + for (int start = 0; start < values.length(); ) { + int end = values.indexOf('\0', start); + if (end == -1) { + end = values.length(); } + result.namesAndValues.add(name); + result.namesAndValues.add(values.substring(start, end)); + start = end + 1; + } } - - public void addAll(String fieldName, List headerFields) { - for (String value : headerFields) { - add(fieldName, value); - } - } - - /** - * Set a field with the specified value. If the field is not found, it is - * added. If the field is found, the existing values are replaced. - */ - public void set(String fieldName, String value) { - removeAll(fieldName); - add(fieldName, value); - } - - /** - * Returns the number of field values. - */ - public int length() { - return namesAndValues.size() / 2; - } - - /** - * Returns the field at {@code position} or null if that is out of range. - */ - public String getFieldName(int index) { - int fieldNameIndex = index * 2; - if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.size()) { - return null; - } - return namesAndValues.get(fieldNameIndex); - } - - /** - * Returns the value at {@code index} or null if that is out of range. - */ - public String getValue(int index) { - int valueIndex = index * 2 + 1; - if (valueIndex < 0 || valueIndex >= namesAndValues.size()) { - return null; - } - return namesAndValues.get(valueIndex); - } - - /** - * Returns the last value corresponding to the specified field, or null. - */ - public String get(String fieldName) { - for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) { - if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) { - return namesAndValues.get(i + 1); - } - } - return null; - } - - /** - * @param fieldNames a case-insensitive set of HTTP header field names. - */ - public RawHeaders getAll(Set fieldNames) { - RawHeaders result = new RawHeaders(); - for (int i = 0; i < namesAndValues.size(); i += 2) { - String fieldName = namesAndValues.get(i); - if (fieldNames.contains(fieldName)) { - result.add(fieldName, namesAndValues.get(i + 1)); - } - } - return result; - } - - /** - * Returns bytes of a request header for sending on an HTTP transport. - */ - public byte[] toBytes() throws UnsupportedEncodingException { - StringBuilder result = new StringBuilder(256); - result.append(requestLine).append("\r\n"); - for (int i = 0; i < namesAndValues.size(); i += 2) { - result.append(namesAndValues.get(i)).append(": ") - .append(namesAndValues.get(i + 1)).append("\r\n"); - } - result.append("\r\n"); - return result.toString().getBytes("ISO-8859-1"); - } - - /** - * Parses bytes of a response header from an HTTP transport. - */ - public static RawHeaders fromBytes(InputStream in) throws IOException { - RawHeaders headers; - do { - headers = new RawHeaders(); - headers.setStatusLine(Util.readAsciiLine(in)); - readHeaders(in, headers); - } while (headers.getResponseCode() == HttpEngine.HTTP_CONTINUE); - return headers; - } - - /** - * Reads headers or trailers into {@code out}. - */ - public static void readHeaders(InputStream in, RawHeaders out) throws IOException { - // parse the result headers until the first blank line - String line; - while ((line = Util.readAsciiLine(in)).length() != 0) { - out.addLine(line); - } - } - - /** - * Returns an immutable map containing each field to its list of values. The - * status line is mapped to null. - */ - public Map> toMultimap(boolean response) { - Map> result = new TreeMap>(FIELD_NAME_COMPARATOR); - for (int i = 0; i < namesAndValues.size(); i += 2) { - String fieldName = namesAndValues.get(i); - String value = namesAndValues.get(i + 1); - - List allValues = new ArrayList(); - List otherValues = result.get(fieldName); - if (otherValues != null) { - allValues.addAll(otherValues); - } - allValues.add(value); - result.put(fieldName, Collections.unmodifiableList(allValues)); - } - if (response && statusLine != null) { - result.put(null, Collections.unmodifiableList(Collections.singletonList(statusLine))); - } else if (requestLine != null) { - result.put(null, Collections.unmodifiableList(Collections.singletonList(requestLine))); - } - return Collections.unmodifiableMap(result); - } - - /** - * Creates a new instance from the given map of fields to values. If - * present, the null field's last element will be used to set the status - * line. - */ - public static RawHeaders fromMultimap(Map> map, boolean response) - throws IOException { - if (!response) throw new UnsupportedOperationException(); - RawHeaders result = new RawHeaders(); - for (Entry> entry : map.entrySet()) { - String fieldName = entry.getKey(); - List values = entry.getValue(); - if (fieldName != null) { - result.addAll(fieldName, values); - } else if (!values.isEmpty()) { - result.setStatusLine(values.get(values.size() - 1)); - } - } - return result; - } - - /** - * Returns a list of alternating names and values. Names are all lower case. - * No names are repeated. If any name has multiple values, they are - * concatenated using "\0" as a delimiter. - */ - public List toNameValueBlock() { - Set names = new HashSet(); - List result = new ArrayList(); - for (int i = 0; i < namesAndValues.size(); i += 2) { - String name = namesAndValues.get(i).toLowerCase(Locale.US); - String value = namesAndValues.get(i + 1); - - // TODO: promote this check to where names and values are created - if (name.length() == 0 || value.length() == 0 - || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) { - throw new IllegalArgumentException("Unexpected header: " + name + ": " + value); - } - - // 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; - } - - // If we haven't seen this name before, add the pair to the end of the list... - if (names.add(name)) { - result.add(name); - result.add(value); - continue; - } - - // ...otherwise concatenate the existing values and this value. - for (int j = 0; j < result.size(); j += 2) { - if (name.equals(result.get(j))) { - result.set(j + 1, result.get(j + 1) + "\0" + value); - break; - } - } - } - return result; - } - - public static RawHeaders fromNameValueBlock(List nameValueBlock) { - if (nameValueBlock.size() % 2 != 0) { - throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock); - } - RawHeaders result = new RawHeaders(); - for (int i = 0; i < nameValueBlock.size(); i += 2) { - String name = nameValueBlock.get(i); - String values = nameValueBlock.get(i + 1); - for (int start = 0; start < values.length();) { - int end = values.indexOf('\0', start); - if (end == -1) { - end = values.length(); - } - result.namesAndValues.add(name); - result.namesAndValues.add(values.substring(start, end)); - start = end + 1; - } - } - return result; - } + return result; + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java index 2c12b9583..2544ceed5 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java +++ b/src/main/java/com/squareup/okhttp/internal/http/RequestHeaders.java @@ -21,272 +21,270 @@ import java.util.Date; import java.util.List; import java.util.Map; -/** - * Parsed HTTP request headers. - */ +/** Parsed HTTP request headers. */ final class RequestHeaders { - private final URI uri; - private final RawHeaders headers; + private final URI uri; + private final RawHeaders headers; - /** Don't use a cache to satisfy this request. */ - private boolean noCache; - private int maxAgeSeconds = -1; - private int maxStaleSeconds = -1; - private int minFreshSeconds = -1; + /** Don't use a cache to satisfy this request. */ + private boolean noCache; + private int maxAgeSeconds = -1; + private int maxStaleSeconds = -1; + private int minFreshSeconds = -1; - /** - * This field's name "only-if-cached" is misleading. It actually means "do - * not use the network". It is set by a client who only wants to make a - * request if it can be fully satisfied by the cache. Cached responses that - * would require validation (ie. conditional gets) are not permitted if this - * header is set. - */ - private boolean onlyIfCached; + /** + * This field's name "only-if-cached" is misleading. It actually means "do + * not use the network". It is set by a client who only wants to make a + * request if it can be fully satisfied by the cache. Cached responses that + * would require validation (ie. conditional gets) are not permitted if this + * header is set. + */ + private boolean onlyIfCached; - /** - * True if the request contains an authorization field. Although this isn't - * necessarily a shared cache, it follows the spec's strict requirements for - * shared caches. - */ - private boolean hasAuthorization; + /** + * True if the request contains an authorization field. Although this isn't + * necessarily a shared cache, it follows the spec's strict requirements for + * shared caches. + */ + private boolean hasAuthorization; - private int contentLength = -1; - private String transferEncoding; - private String userAgent; - private String host; - private String connection; - private String acceptEncoding; - private String contentType; - private String ifModifiedSince; - private String ifNoneMatch; - private String proxyAuthorization; + private int contentLength = -1; + private String transferEncoding; + private String userAgent; + private String host; + private String connection; + private String acceptEncoding; + private String contentType; + private String ifModifiedSince; + private String ifNoneMatch; + private String proxyAuthorization; - public RequestHeaders(URI uri, RawHeaders headers) { - this.uri = uri; - this.headers = headers; + public RequestHeaders(URI uri, RawHeaders headers) { + this.uri = uri; + this.headers = headers; - HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { - @Override public void handle(String directive, String parameter) { - if ("no-cache".equalsIgnoreCase(directive)) { - noCache = true; - } else if ("max-age".equalsIgnoreCase(directive)) { - maxAgeSeconds = HeaderParser.parseSeconds(parameter); - } else if ("max-stale".equalsIgnoreCase(directive)) { - maxStaleSeconds = HeaderParser.parseSeconds(parameter); - } else if ("min-fresh".equalsIgnoreCase(directive)) { - minFreshSeconds = HeaderParser.parseSeconds(parameter); - } else if ("only-if-cached".equalsIgnoreCase(directive)) { - onlyIfCached = true; - } - } - }; - - for (int i = 0; i < headers.length(); i++) { - String fieldName = headers.getFieldName(i); - String value = headers.getValue(i); - if ("Cache-Control".equalsIgnoreCase(fieldName)) { - HeaderParser.parseCacheControl(value, handler); - } else if ("Pragma".equalsIgnoreCase(fieldName)) { - if ("no-cache".equalsIgnoreCase(value)) { - noCache = true; - } - } else if ("If-None-Match".equalsIgnoreCase(fieldName)) { - ifNoneMatch = value; - } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) { - ifModifiedSince = value; - } else if ("Authorization".equalsIgnoreCase(fieldName)) { - hasAuthorization = true; - } else if ("Content-Length".equalsIgnoreCase(fieldName)) { - try { - contentLength = Integer.parseInt(value); - } catch (NumberFormatException ignored) { - } - } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { - transferEncoding = value; - } else if ("User-Agent".equalsIgnoreCase(fieldName)) { - userAgent = value; - } else if ("Host".equalsIgnoreCase(fieldName)) { - host = value; - } else if ("Connection".equalsIgnoreCase(fieldName)) { - connection = value; - } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) { - acceptEncoding = value; - } else if ("Content-Type".equalsIgnoreCase(fieldName)) { - contentType = value; - } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) { - proxyAuthorization = value; - } + HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { + @Override public void handle(String directive, String parameter) { + if ("no-cache".equalsIgnoreCase(directive)) { + noCache = true; + } else if ("max-age".equalsIgnoreCase(directive)) { + maxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("max-stale".equalsIgnoreCase(directive)) { + maxStaleSeconds = HeaderParser.parseSeconds(parameter); + } else if ("min-fresh".equalsIgnoreCase(directive)) { + minFreshSeconds = HeaderParser.parseSeconds(parameter); + } else if ("only-if-cached".equalsIgnoreCase(directive)) { + onlyIfCached = true; } - } + } + }; - public boolean isChunked() { - return "chunked".equalsIgnoreCase(transferEncoding); - } - - public boolean hasConnectionClose() { - return "close".equalsIgnoreCase(connection); - } - - public URI getUri() { - return uri; - } - - public RawHeaders getHeaders() { - return headers; - } - - public boolean isNoCache() { - return noCache; - } - - public int getMaxAgeSeconds() { - return maxAgeSeconds; - } - - public int getMaxStaleSeconds() { - return maxStaleSeconds; - } - - public int getMinFreshSeconds() { - return minFreshSeconds; - } - - public boolean isOnlyIfCached() { - return onlyIfCached; - } - - public boolean hasAuthorization() { - return hasAuthorization; - } - - public int getContentLength() { - return contentLength; - } - - public String getTransferEncoding() { - return transferEncoding; - } - - public String getUserAgent() { - return userAgent; - } - - public String getHost() { - return host; - } - - public String getConnection() { - return connection; - } - - public String getAcceptEncoding() { - return acceptEncoding; - } - - public String getContentType() { - return contentType; - } - - public String getIfModifiedSince() { - return ifModifiedSince; - } - - public String getIfNoneMatch() { - return ifNoneMatch; - } - - public String getProxyAuthorization() { - return proxyAuthorization; - } - - public void setChunked() { - if (this.transferEncoding != null) { - headers.removeAll("Transfer-Encoding"); + for (int i = 0; i < headers.length(); i++) { + String fieldName = headers.getFieldName(i); + String value = headers.getValue(i); + if ("Cache-Control".equalsIgnoreCase(fieldName)) { + HeaderParser.parseCacheControl(value, handler); + } else if ("Pragma".equalsIgnoreCase(fieldName)) { + if ("no-cache".equalsIgnoreCase(value)) { + noCache = true; } - headers.add("Transfer-Encoding", "chunked"); - this.transferEncoding = "chunked"; - } - - public void setContentLength(int contentLength) { - if (this.contentLength != -1) { - headers.removeAll("Content-Length"); + } else if ("If-None-Match".equalsIgnoreCase(fieldName)) { + ifNoneMatch = value; + } else if ("If-Modified-Since".equalsIgnoreCase(fieldName)) { + ifModifiedSince = value; + } else if ("Authorization".equalsIgnoreCase(fieldName)) { + hasAuthorization = true; + } else if ("Content-Length".equalsIgnoreCase(fieldName)) { + try { + contentLength = Integer.parseInt(value); + } catch (NumberFormatException ignored) { } - headers.add("Content-Length", Integer.toString(contentLength)); - this.contentLength = contentLength; + } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { + transferEncoding = value; + } else if ("User-Agent".equalsIgnoreCase(fieldName)) { + userAgent = value; + } else if ("Host".equalsIgnoreCase(fieldName)) { + host = value; + } else if ("Connection".equalsIgnoreCase(fieldName)) { + connection = value; + } else if ("Accept-Encoding".equalsIgnoreCase(fieldName)) { + acceptEncoding = value; + } else if ("Content-Type".equalsIgnoreCase(fieldName)) { + contentType = value; + } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) { + proxyAuthorization = value; + } } + } - public void setUserAgent(String userAgent) { - if (this.userAgent != null) { - headers.removeAll("User-Agent"); - } - headers.add("User-Agent", userAgent); - this.userAgent = userAgent; - } + public boolean isChunked() { + return "chunked".equalsIgnoreCase(transferEncoding); + } - public void setHost(String host) { - if (this.host != null) { - headers.removeAll("Host"); - } - headers.add("Host", host); - this.host = host; - } + public boolean hasConnectionClose() { + return "close".equalsIgnoreCase(connection); + } - public void setConnection(String connection) { - if (this.connection != null) { - headers.removeAll("Connection"); - } - headers.add("Connection", connection); - this.connection = connection; - } + public URI getUri() { + return uri; + } - public void setAcceptEncoding(String acceptEncoding) { - if (this.acceptEncoding != null) { - headers.removeAll("Accept-Encoding"); - } - headers.add("Accept-Encoding", acceptEncoding); - this.acceptEncoding = acceptEncoding; - } + public RawHeaders getHeaders() { + return headers; + } - public void setContentType(String contentType) { - if (this.contentType != null) { - headers.removeAll("Content-Type"); - } - headers.add("Content-Type", contentType); - this.contentType = contentType; - } + public boolean isNoCache() { + return noCache; + } - public void setIfModifiedSince(Date date) { - if (ifModifiedSince != null) { - headers.removeAll("If-Modified-Since"); - } - String formattedDate = HttpDate.format(date); - headers.add("If-Modified-Since", formattedDate); - ifModifiedSince = formattedDate; - } + public int getMaxAgeSeconds() { + return maxAgeSeconds; + } - public void setIfNoneMatch(String ifNoneMatch) { - if (this.ifNoneMatch != null) { - headers.removeAll("If-None-Match"); - } - headers.add("If-None-Match", ifNoneMatch); - this.ifNoneMatch = ifNoneMatch; - } + public int getMaxStaleSeconds() { + return maxStaleSeconds; + } - /** - * Returns true if the request contains conditions that save the server from - * sending a response that the client has locally. When the caller adds - * conditions, this cache won't participate in the request. - */ - public boolean hasConditions() { - return ifModifiedSince != null || ifNoneMatch != null; - } + public int getMinFreshSeconds() { + return minFreshSeconds; + } - public void addCookies(Map> allCookieHeaders) { - for (Map.Entry> entry : allCookieHeaders.entrySet()) { - String key = entry.getKey(); - if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) { - headers.addAll(key, entry.getValue()); - } - } + public boolean isOnlyIfCached() { + return onlyIfCached; + } + + public boolean hasAuthorization() { + return hasAuthorization; + } + + public int getContentLength() { + return contentLength; + } + + public String getTransferEncoding() { + return transferEncoding; + } + + public String getUserAgent() { + return userAgent; + } + + public String getHost() { + return host; + } + + public String getConnection() { + return connection; + } + + public String getAcceptEncoding() { + return acceptEncoding; + } + + public String getContentType() { + return contentType; + } + + public String getIfModifiedSince() { + return ifModifiedSince; + } + + public String getIfNoneMatch() { + return ifNoneMatch; + } + + public String getProxyAuthorization() { + return proxyAuthorization; + } + + public void setChunked() { + if (this.transferEncoding != null) { + headers.removeAll("Transfer-Encoding"); } + headers.add("Transfer-Encoding", "chunked"); + this.transferEncoding = "chunked"; + } + + public void setContentLength(int contentLength) { + if (this.contentLength != -1) { + headers.removeAll("Content-Length"); + } + headers.add("Content-Length", Integer.toString(contentLength)); + this.contentLength = contentLength; + } + + public void setUserAgent(String userAgent) { + if (this.userAgent != null) { + headers.removeAll("User-Agent"); + } + headers.add("User-Agent", userAgent); + this.userAgent = userAgent; + } + + public void setHost(String host) { + if (this.host != null) { + headers.removeAll("Host"); + } + headers.add("Host", host); + this.host = host; + } + + public void setConnection(String connection) { + if (this.connection != null) { + headers.removeAll("Connection"); + } + headers.add("Connection", connection); + this.connection = connection; + } + + public void setAcceptEncoding(String acceptEncoding) { + if (this.acceptEncoding != null) { + headers.removeAll("Accept-Encoding"); + } + headers.add("Accept-Encoding", acceptEncoding); + this.acceptEncoding = acceptEncoding; + } + + public void setContentType(String contentType) { + if (this.contentType != null) { + headers.removeAll("Content-Type"); + } + headers.add("Content-Type", contentType); + this.contentType = contentType; + } + + public void setIfModifiedSince(Date date) { + if (ifModifiedSince != null) { + headers.removeAll("If-Modified-Since"); + } + String formattedDate = HttpDate.format(date); + headers.add("If-Modified-Since", formattedDate); + ifModifiedSince = formattedDate; + } + + public void setIfNoneMatch(String ifNoneMatch) { + if (this.ifNoneMatch != null) { + headers.removeAll("If-None-Match"); + } + headers.add("If-None-Match", ifNoneMatch); + this.ifNoneMatch = ifNoneMatch; + } + + /** + * Returns true if the request contains conditions that save the server from + * sending a response that the client has locally. When the caller adds + * conditions, this cache won't participate in the request. + */ + public boolean hasConditions() { + return ifModifiedSince != null || ifNoneMatch != null; + } + + public void addCookies(Map> allCookieHeaders) { + for (Map.Entry> entry : allCookieHeaders.entrySet()) { + String key = entry.getKey(); + if ("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key)) { + headers.addAll(key, entry.getValue()); + } + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java index d333d8b45..25288995c 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java +++ b/src/main/java/com/squareup/okhttp/internal/http/ResponseHeaders.java @@ -17,7 +17,6 @@ package com.squareup.okhttp.internal.http; import com.squareup.okhttp.ResponseSource; -import static com.squareup.okhttp.internal.Util.equal; import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; @@ -29,476 +28,467 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.TimeUnit; -/** - * Parsed HTTP response headers. - */ +import static com.squareup.okhttp.internal.Util.equal; + +/** Parsed HTTP response headers. */ final class ResponseHeaders { - /** HTTP header name for the local time when the request was sent. */ - private static final String SENT_MILLIS = "X-Android-Sent-Millis"; + /** HTTP header name for the local time when the request was sent. */ + private static final String SENT_MILLIS = "X-Android-Sent-Millis"; - /** HTTP header name for the local time when the response was received. */ - private static final String RECEIVED_MILLIS = "X-Android-Received-Millis"; + /** HTTP header name for the local time when the response was received. */ + private static final String RECEIVED_MILLIS = "X-Android-Received-Millis"; - private final URI uri; - private final RawHeaders headers; + private final URI uri; + private final RawHeaders headers; - /** The server's time when this response was served, if known. */ - private Date servedDate; + /** The server's time when this response was served, if known. */ + private Date servedDate; - /** The last modified date of the response, if known. */ - private Date lastModified; + /** The last modified date of the response, if known. */ + private Date lastModified; - /** - * The expiration date of the response, if known. If both this field and the - * max age are set, the max age is preferred. - */ - private Date expires; + /** + * The expiration date of the response, if known. If both this field and the + * max age are set, the max age is preferred. + */ + private Date expires; - /** - * Extension header set by HttpURLConnectionImpl specifying the timestamp - * when the HTTP request was first initiated. - */ - private long sentRequestMillis; + /** + * Extension header set by HttpURLConnectionImpl specifying the timestamp + * when the HTTP request was first initiated. + */ + private long sentRequestMillis; - /** - * Extension header set by HttpURLConnectionImpl specifying the timestamp - * when the HTTP response was first received. - */ - private long receivedResponseMillis; + /** + * Extension header set by HttpURLConnectionImpl specifying the timestamp + * when the HTTP response was first received. + */ + private long receivedResponseMillis; - /** - * In the response, this field's name "no-cache" is misleading. It doesn't - * prevent us from caching the response; it only means we have to validate - * the response with the origin server before returning it. We can do this - * with a conditional get. - */ - private boolean noCache; + /** + * In the response, this field's name "no-cache" is misleading. It doesn't + * prevent us from caching the response; it only means we have to validate + * the response with the origin server before returning it. We can do this + * with a conditional get. + */ + private boolean noCache; - /** If true, this response should not be cached. */ - private boolean noStore; + /** If true, this response should not be cached. */ + private boolean noStore; - /** - * The duration past the response's served date that it can be served - * without validation. - */ - private int maxAgeSeconds = -1; + /** + * The duration past the response's served date that it can be served + * without validation. + */ + private int maxAgeSeconds = -1; - /** - * The "s-maxage" directive is the max age for shared caches. Not to be - * confused with "max-age" for non-shared caches, As in Firefox and Chrome, - * this directive is not honored by this cache. - */ - private int sMaxAgeSeconds = -1; + /** + * The "s-maxage" directive is the max age for shared caches. Not to be + * confused with "max-age" for non-shared caches, As in Firefox and Chrome, + * this directive is not honored by this cache. + */ + private int sMaxAgeSeconds = -1; - /** - * This request header field's name "only-if-cached" is misleading. It - * actually means "do not use the network". It is set by a client who only - * wants to make a request if it can be fully satisfied by the cache. - * Cached responses that would require validation (ie. conditional gets) are - * not permitted if this header is set. - */ - private boolean isPublic; - private boolean mustRevalidate; - private String etag; - private int ageSeconds = -1; + /** + * This request header field's name "only-if-cached" is misleading. It + * actually means "do not use the network". It is set by a client who only + * wants to make a request if it can be fully satisfied by the cache. + * Cached responses that would require validation (ie. conditional gets) are + * not permitted if this header is set. + */ + private boolean isPublic; + private boolean mustRevalidate; + private String etag; + private int ageSeconds = -1; - /** Case-insensitive set of field names. */ - private Set varyFields = Collections.emptySet(); + /** Case-insensitive set of field names. */ + private Set varyFields = Collections.emptySet(); - private String contentEncoding; - private String transferEncoding; - private int contentLength = -1; - private String connection; + private String contentEncoding; + private String transferEncoding; + private int contentLength = -1; + private String connection; - public ResponseHeaders(URI uri, RawHeaders headers) { - this.uri = uri; - this.headers = headers; + public ResponseHeaders(URI uri, RawHeaders headers) { + this.uri = uri; + this.headers = headers; - HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { - @Override public void handle(String directive, String parameter) { - if ("no-cache".equalsIgnoreCase(directive)) { - noCache = true; - } else if ("no-store".equalsIgnoreCase(directive)) { - noStore = true; - } else if ("max-age".equalsIgnoreCase(directive)) { - maxAgeSeconds = HeaderParser.parseSeconds(parameter); - } else if ("s-maxage".equalsIgnoreCase(directive)) { - sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); - } else if ("public".equalsIgnoreCase(directive)) { - isPublic = true; - } else if ("must-revalidate".equalsIgnoreCase(directive)) { - mustRevalidate = true; - } - } - }; - - for (int i = 0; i < headers.length(); i++) { - String fieldName = headers.getFieldName(i); - String value = headers.getValue(i); - if ("Cache-Control".equalsIgnoreCase(fieldName)) { - HeaderParser.parseCacheControl(value, handler); - } else if ("Date".equalsIgnoreCase(fieldName)) { - servedDate = HttpDate.parse(value); - } else if ("Expires".equalsIgnoreCase(fieldName)) { - expires = HttpDate.parse(value); - } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { - lastModified = HttpDate.parse(value); - } else if ("ETag".equalsIgnoreCase(fieldName)) { - etag = value; - } else if ("Pragma".equalsIgnoreCase(fieldName)) { - if ("no-cache".equalsIgnoreCase(value)) { - noCache = true; - } - } else if ("Age".equalsIgnoreCase(fieldName)) { - ageSeconds = HeaderParser.parseSeconds(value); - } else if ("Vary".equalsIgnoreCase(fieldName)) { - // Replace the immutable empty set with something we can mutate. - if (varyFields.isEmpty()) { - varyFields = new TreeSet(String.CASE_INSENSITIVE_ORDER); - } - for (String varyField : value.split(",")) { - varyFields.add(varyField.trim()); - } - } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { - contentEncoding = value; - } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { - transferEncoding = value; - } else if ("Content-Length".equalsIgnoreCase(fieldName)) { - try { - contentLength = Integer.parseInt(value); - } catch (NumberFormatException ignored) { - } - } else if ("Connection".equalsIgnoreCase(fieldName)) { - connection = value; - } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) { - sentRequestMillis = Long.parseLong(value); - } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { - receivedResponseMillis = Long.parseLong(value); - } + HeaderParser.CacheControlHandler handler = new HeaderParser.CacheControlHandler() { + @Override public void handle(String directive, String parameter) { + if ("no-cache".equalsIgnoreCase(directive)) { + noCache = true; + } else if ("no-store".equalsIgnoreCase(directive)) { + noStore = true; + } else if ("max-age".equalsIgnoreCase(directive)) { + maxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("s-maxage".equalsIgnoreCase(directive)) { + sMaxAgeSeconds = HeaderParser.parseSeconds(parameter); + } else if ("public".equalsIgnoreCase(directive)) { + isPublic = true; + } else if ("must-revalidate".equalsIgnoreCase(directive)) { + mustRevalidate = true; } - } + } + }; - public boolean isContentEncodingGzip() { - return "gzip".equalsIgnoreCase(contentEncoding); + for (int i = 0; i < headers.length(); i++) { + String fieldName = headers.getFieldName(i); + String value = headers.getValue(i); + if ("Cache-Control".equalsIgnoreCase(fieldName)) { + HeaderParser.parseCacheControl(value, handler); + } else if ("Date".equalsIgnoreCase(fieldName)) { + servedDate = HttpDate.parse(value); + } else if ("Expires".equalsIgnoreCase(fieldName)) { + expires = HttpDate.parse(value); + } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { + lastModified = HttpDate.parse(value); + } else if ("ETag".equalsIgnoreCase(fieldName)) { + etag = value; + } else if ("Pragma".equalsIgnoreCase(fieldName)) { + if ("no-cache".equalsIgnoreCase(value)) { + noCache = true; + } + } else if ("Age".equalsIgnoreCase(fieldName)) { + ageSeconds = HeaderParser.parseSeconds(value); + } else if ("Vary".equalsIgnoreCase(fieldName)) { + // Replace the immutable empty set with something we can mutate. + if (varyFields.isEmpty()) { + varyFields = new TreeSet(String.CASE_INSENSITIVE_ORDER); + } + for (String varyField : value.split(",")) { + varyFields.add(varyField.trim()); + } + } else if ("Content-Encoding".equalsIgnoreCase(fieldName)) { + contentEncoding = value; + } else if ("Transfer-Encoding".equalsIgnoreCase(fieldName)) { + transferEncoding = value; + } else if ("Content-Length".equalsIgnoreCase(fieldName)) { + try { + contentLength = Integer.parseInt(value); + } catch (NumberFormatException ignored) { + } + } else if ("Connection".equalsIgnoreCase(fieldName)) { + connection = value; + } else if (SENT_MILLIS.equalsIgnoreCase(fieldName)) { + sentRequestMillis = Long.parseLong(value); + } else if (RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) { + receivedResponseMillis = Long.parseLong(value); + } } + } - public void stripContentEncoding() { - contentEncoding = null; - headers.removeAll("Content-Encoding"); - } + public boolean isContentEncodingGzip() { + return "gzip".equalsIgnoreCase(contentEncoding); + } - public boolean isChunked() { - return "chunked".equalsIgnoreCase(transferEncoding); - } + public void stripContentEncoding() { + contentEncoding = null; + headers.removeAll("Content-Encoding"); + } - public boolean hasConnectionClose() { - return "close".equalsIgnoreCase(connection); - } + public boolean isChunked() { + return "chunked".equalsIgnoreCase(transferEncoding); + } - public URI getUri() { - return uri; - } + public boolean hasConnectionClose() { + return "close".equalsIgnoreCase(connection); + } - public RawHeaders getHeaders() { - return headers; - } + public URI getUri() { + return uri; + } - public Date getServedDate() { - return servedDate; - } + public RawHeaders getHeaders() { + return headers; + } - public Date getLastModified() { - return lastModified; - } + public Date getServedDate() { + return servedDate; + } - public Date getExpires() { - return expires; - } + public Date getLastModified() { + return lastModified; + } - public boolean isNoCache() { - return noCache; - } + public Date getExpires() { + return expires; + } - public boolean isNoStore() { - return noStore; - } + public boolean isNoCache() { + return noCache; + } - public int getMaxAgeSeconds() { - return maxAgeSeconds; - } + public boolean isNoStore() { + return noStore; + } - public int getSMaxAgeSeconds() { - return sMaxAgeSeconds; - } + public int getMaxAgeSeconds() { + return maxAgeSeconds; + } - public boolean isPublic() { - return isPublic; - } + public int getSMaxAgeSeconds() { + return sMaxAgeSeconds; + } - public boolean isMustRevalidate() { - return mustRevalidate; - } + public boolean isPublic() { + return isPublic; + } - public String getEtag() { - return etag; - } + public boolean isMustRevalidate() { + return mustRevalidate; + } - public Set getVaryFields() { - return varyFields; - } + public String getEtag() { + return etag; + } - public String getContentEncoding() { - return contentEncoding; - } + public Set getVaryFields() { + return varyFields; + } - public int getContentLength() { - return contentLength; - } + public String getContentEncoding() { + return contentEncoding; + } - public String getConnection() { - return connection; - } + public int getContentLength() { + return contentLength; + } - public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) { - this.sentRequestMillis = sentRequestMillis; - headers.add(SENT_MILLIS, Long.toString(sentRequestMillis)); - this.receivedResponseMillis = receivedResponseMillis; - headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); - } + public String getConnection() { + return connection; + } - /** - * Returns the current age of the response, in milliseconds. The calculation - * is specified by RFC 2616, 13.2.3 Age Calculations. - */ - private long computeAge(long nowMillis) { - long apparentReceivedAge = servedDate != null - ? Math.max(0, receivedResponseMillis - servedDate.getTime()) - : 0; - long receivedAge = ageSeconds != -1 - ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) - : apparentReceivedAge; - long responseDuration = receivedResponseMillis - sentRequestMillis; - long residentDuration = nowMillis - receivedResponseMillis; - return receivedAge + responseDuration + residentDuration; - } + public void setLocalTimestamps(long sentRequestMillis, long receivedResponseMillis) { + this.sentRequestMillis = sentRequestMillis; + headers.add(SENT_MILLIS, Long.toString(sentRequestMillis)); + this.receivedResponseMillis = receivedResponseMillis; + headers.add(RECEIVED_MILLIS, Long.toString(receivedResponseMillis)); + } - /** - * Returns the number of milliseconds that the response was fresh for, - * starting from the served date. - */ - private long computeFreshnessLifetime() { - if (maxAgeSeconds != -1) { - return TimeUnit.SECONDS.toMillis(maxAgeSeconds); - } else if (expires != null) { - long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; - long delta = expires.getTime() - servedMillis; - return delta > 0 ? delta : 0; - } else if (lastModified != null && uri.getRawQuery() == null) { + /** + * Returns the current age of the response, in milliseconds. The calculation + * is specified by RFC 2616, 13.2.3 Age Calculations. + */ + private long computeAge(long nowMillis) { + long apparentReceivedAge = + servedDate != null ? Math.max(0, receivedResponseMillis - servedDate.getTime()) : 0; + long receivedAge = + ageSeconds != -1 ? Math.max(apparentReceivedAge, TimeUnit.SECONDS.toMillis(ageSeconds)) + : apparentReceivedAge; + long responseDuration = receivedResponseMillis - sentRequestMillis; + long residentDuration = nowMillis - receivedResponseMillis; + return receivedAge + responseDuration + residentDuration; + } + + /** + * Returns the number of milliseconds that the response was fresh for, + * starting from the served date. + */ + private long computeFreshnessLifetime() { + if (maxAgeSeconds != -1) { + return TimeUnit.SECONDS.toMillis(maxAgeSeconds); + } else if (expires != null) { + long servedMillis = servedDate != null ? servedDate.getTime() : receivedResponseMillis; + long delta = expires.getTime() - servedMillis; + return delta > 0 ? delta : 0; + } else if (lastModified != null && uri.getRawQuery() == null) { /* * As recommended by the HTTP RFC and implemented in Firefox, the * max age of a document should be defaulted to 10% of the * document's age at the time it was served. Default expiration * dates aren't used for URIs containing a query. */ - long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; - long delta = servedMillis - lastModified.getTime(); - return delta > 0 ? (delta / 10) : 0; - } - return 0; + long servedMillis = servedDate != null ? servedDate.getTime() : sentRequestMillis; + long delta = servedMillis - lastModified.getTime(); + return delta > 0 ? (delta / 10) : 0; } + return 0; + } - /** - * Returns true if computeFreshnessLifetime used a heuristic. If we used a - * heuristic to serve a cached response older than 24 hours, we are required - * to attach a warning. - */ - private boolean isFreshnessLifetimeHeuristic() { - return maxAgeSeconds == -1 && expires == null; - } + /** + * Returns true if computeFreshnessLifetime used a heuristic. If we used a + * heuristic to serve a cached response older than 24 hours, we are required + * to attach a warning. + */ + private boolean isFreshnessLifetimeHeuristic() { + return maxAgeSeconds == -1 && expires == null; + } - /** - * Returns true if this response can be stored to later serve another - * request. - */ - public boolean isCacheable(RequestHeaders request) { + /** + * Returns true if this response can be stored to later serve another + * request. + */ + public boolean isCacheable(RequestHeaders request) { /* * Always go to network for uncacheable response codes (RFC 2616, 13.4), * This implementation doesn't support caching partial content. */ - int responseCode = headers.getResponseCode(); - if (responseCode != HttpURLConnection.HTTP_OK - && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE - && responseCode != HttpURLConnection.HTTP_MULT_CHOICE - && responseCode != HttpURLConnection.HTTP_MOVED_PERM - && responseCode != HttpURLConnection.HTTP_GONE) { - return false; - } + int responseCode = headers.getResponseCode(); + if (responseCode != HttpURLConnection.HTTP_OK + && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE + && responseCode != HttpURLConnection.HTTP_MULT_CHOICE + && responseCode != HttpURLConnection.HTTP_MOVED_PERM + && responseCode != HttpURLConnection.HTTP_GONE) { + return false; + } /* * Responses to authorized requests aren't cacheable unless they include * a 'public', 'must-revalidate' or 's-maxage' directive. */ - if (request.hasAuthorization() - && !isPublic - && !mustRevalidate - && sMaxAgeSeconds == -1) { - return false; - } - - if (noStore) { - return false; - } - - return true; + if (request.hasAuthorization() && !isPublic && !mustRevalidate && sMaxAgeSeconds == -1) { + return false; } - /** - * Returns true if a Vary header contains an asterisk. Such responses cannot - * be cached. - */ - public boolean hasVaryAll() { - return varyFields.contains("*"); + if (noStore) { + return false; } - /** - * Returns true if none of the Vary headers on this response have changed - * between {@code cachedRequest} and {@code newRequest}. - */ - public boolean varyMatches(Map> cachedRequest, - Map> newRequest) { - for (String field : varyFields) { - if (!equal(cachedRequest.get(field), newRequest.get(field))) { - return false; - } - } - return true; - } + return true; + } - /** - * Returns the source to satisfy {@code request} given this cached response. - */ - public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) { + /** + * Returns true if a Vary header contains an asterisk. Such responses cannot + * be cached. + */ + public boolean hasVaryAll() { + return varyFields.contains("*"); + } + + /** + * Returns true if none of the Vary headers on this response have changed + * between {@code cachedRequest} and {@code newRequest}. + */ + public boolean varyMatches(Map> cachedRequest, + Map> newRequest) { + for (String field : varyFields) { + if (!equal(cachedRequest.get(field), newRequest.get(field))) { + return false; + } + } + return true; + } + + /** Returns the source to satisfy {@code request} given this cached response. */ + public ResponseSource chooseResponseSource(long nowMillis, RequestHeaders request) { /* * If this response shouldn't have been stored, it should never be used * as a response source. This check should be redundant as long as the * persistence store is well-behaved and the rules are constant. */ - if (!isCacheable(request)) { - return ResponseSource.NETWORK; - } - - if (request.isNoCache() || request.hasConditions()) { - return ResponseSource.NETWORK; - } - - long ageMillis = computeAge(nowMillis); - long freshMillis = computeFreshnessLifetime(); - - if (request.getMaxAgeSeconds() != -1) { - freshMillis = Math.min(freshMillis, - TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); - } - - long minFreshMillis = 0; - if (request.getMinFreshSeconds() != -1) { - minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); - } - - long maxStaleMillis = 0; - if (!mustRevalidate && request.getMaxStaleSeconds() != -1) { - maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); - } - - if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { - if (ageMillis + minFreshMillis >= freshMillis) { - headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); - } - if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) { - headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); - } - return ResponseSource.CACHE; - } - - if (lastModified != null) { - request.setIfModifiedSince(lastModified); - } else if (servedDate != null) { - request.setIfModifiedSince(servedDate); - } - - if (etag != null) { - request.setIfNoneMatch(etag); - } - - return request.hasConditions() - ? ResponseSource.CONDITIONAL_CACHE - : ResponseSource.NETWORK; + if (!isCacheable(request)) { + return ResponseSource.NETWORK; } - /** - * Returns true if this cached response should be used; false if the - * network response should be used. - */ - public boolean validate(ResponseHeaders networkResponse) { - if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { - return true; - } + if (request.isNoCache() || request.hasConditions()) { + return ResponseSource.NETWORK; + } + + long ageMillis = computeAge(nowMillis); + long freshMillis = computeFreshnessLifetime(); + + if (request.getMaxAgeSeconds() != -1) { + freshMillis = Math.min(freshMillis, TimeUnit.SECONDS.toMillis(request.getMaxAgeSeconds())); + } + + long minFreshMillis = 0; + if (request.getMinFreshSeconds() != -1) { + minFreshMillis = TimeUnit.SECONDS.toMillis(request.getMinFreshSeconds()); + } + + long maxStaleMillis = 0; + if (!mustRevalidate && request.getMaxStaleSeconds() != -1) { + maxStaleMillis = TimeUnit.SECONDS.toMillis(request.getMaxStaleSeconds()); + } + + if (!noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) { + if (ageMillis + minFreshMillis >= freshMillis) { + headers.add("Warning", "110 HttpURLConnection \"Response is stale\""); + } + if (ageMillis > TimeUnit.HOURS.toMillis(24) && isFreshnessLifetimeHeuristic()) { + headers.add("Warning", "113 HttpURLConnection \"Heuristic expiration\""); + } + return ResponseSource.CACHE; + } + + if (lastModified != null) { + request.setIfModifiedSince(lastModified); + } else if (servedDate != null) { + request.setIfModifiedSince(servedDate); + } + + if (etag != null) { + request.setIfNoneMatch(etag); + } + + return request.hasConditions() ? ResponseSource.CONDITIONAL_CACHE : ResponseSource.NETWORK; + } + + /** + * Returns true if this cached response should be used; false if the + * network response should be used. + */ + public boolean validate(ResponseHeaders networkResponse) { + if (networkResponse.headers.getResponseCode() == HttpURLConnection.HTTP_NOT_MODIFIED) { + return true; + } /* * The HTTP spec says that if the network's response is older than our * cached response, we may return the cache's response. Like Chrome (but * unlike Firefox), this client prefers to return the newer response. */ - if (lastModified != null - && networkResponse.lastModified != null - && networkResponse.lastModified.getTime() < lastModified.getTime()) { - return true; - } - - return false; + if (lastModified != null + && networkResponse.lastModified != null + && networkResponse.lastModified.getTime() < lastModified.getTime()) { + return true; } - /** - * Combines this cached header with a network header as defined by RFC 2616, - * 13.5.3. - */ - public ResponseHeaders combine(ResponseHeaders network) throws IOException { - RawHeaders result = new RawHeaders(); - result.setStatusLine(headers.getStatusLine()); + return false; + } - for (int i = 0; i < headers.length(); i++) { - String fieldName = headers.getFieldName(i); - String value = headers.getValue(i); - if ("Warning".equals(fieldName) && value.startsWith("1")) { - continue; // drop 100-level freshness warnings - } - if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) { - result.add(fieldName, value); - } - } + /** + * Combines this cached header with a network header as defined by RFC 2616, + * 13.5.3. + */ + public ResponseHeaders combine(ResponseHeaders network) throws IOException { + RawHeaders result = new RawHeaders(); + result.setStatusLine(headers.getStatusLine()); - for (int i = 0; i < network.headers.length(); i++) { - String fieldName = network.headers.getFieldName(i); - if (isEndToEnd(fieldName)) { - result.add(fieldName, network.headers.getValue(i)); - } - } - - return new ResponseHeaders(uri, result); + for (int i = 0; i < headers.length(); i++) { + String fieldName = headers.getFieldName(i); + String value = headers.getValue(i); + if ("Warning".equals(fieldName) && value.startsWith("1")) { + continue; // drop 100-level freshness warnings + } + if (!isEndToEnd(fieldName) || network.headers.get(fieldName) == null) { + result.add(fieldName, value); + } } - /** - * Returns true if {@code fieldName} is an end-to-end HTTP header, as - * defined by RFC 2616, 13.5.1. - */ - private static boolean isEndToEnd(String fieldName) { - return !"Connection".equalsIgnoreCase(fieldName) - && !"Keep-Alive".equalsIgnoreCase(fieldName) - && !"Proxy-Authenticate".equalsIgnoreCase(fieldName) - && !"Proxy-Authorization".equalsIgnoreCase(fieldName) - && !"TE".equalsIgnoreCase(fieldName) - && !"Trailers".equalsIgnoreCase(fieldName) - && !"Transfer-Encoding".equalsIgnoreCase(fieldName) - && !"Upgrade".equalsIgnoreCase(fieldName); + for (int i = 0; i < network.headers.length(); i++) { + String fieldName = network.headers.getFieldName(i); + if (isEndToEnd(fieldName)) { + result.add(fieldName, network.headers.getValue(i)); + } } + + return new ResponseHeaders(uri, result); + } + + /** + * Returns true if {@code fieldName} is an end-to-end HTTP header, as + * defined by RFC 2616, 13.5.1. + */ + private static boolean isEndToEnd(String fieldName) { + return !"Connection".equalsIgnoreCase(fieldName) + && !"Keep-Alive".equalsIgnoreCase(fieldName) + && !"Proxy-Authenticate".equalsIgnoreCase(fieldName) + && !"Proxy-Authorization".equalsIgnoreCase(fieldName) + && !"TE".equalsIgnoreCase(fieldName) + && !"Trailers".equalsIgnoreCase(fieldName) + && !"Transfer-Encoding".equalsIgnoreCase(fieldName) + && !"Upgrade".equalsIgnoreCase(fieldName); + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java index a5c5842e9..325327db0 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java +++ b/src/main/java/com/squareup/okhttp/internal/http/RetryableOutputStream.java @@ -16,58 +16,59 @@ package com.squareup.okhttp.internal.http; -import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.net.ProtocolException; +import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; + /** * An HTTP request body that's completely buffered in memory. This allows * the post body to be transparently re-sent if the HTTP request must be * sent multiple times. */ final class RetryableOutputStream extends AbstractHttpOutputStream { - private final int limit; - private final ByteArrayOutputStream content; + private final int limit; + private final ByteArrayOutputStream content; - public RetryableOutputStream(int limit) { - this.limit = limit; - this.content = new ByteArrayOutputStream(limit); - } + public RetryableOutputStream(int limit) { + this.limit = limit; + this.content = new ByteArrayOutputStream(limit); + } - public RetryableOutputStream() { - this.limit = -1; - this.content = new ByteArrayOutputStream(); - } + public RetryableOutputStream() { + this.limit = -1; + this.content = new ByteArrayOutputStream(); + } - @Override public synchronized void close() throws IOException { - if (closed) { - return; - } - closed = true; - if (content.size() < limit) { - throw new ProtocolException("content-length promised " - + limit + " bytes, but received " + content.size()); - } + @Override public synchronized void close() throws IOException { + if (closed) { + return; } + closed = true; + if (content.size() < limit) { + throw new ProtocolException( + "content-length promised " + limit + " bytes, but received " + content.size()); + } + } - @Override public synchronized void write(byte[] buffer, int offset, int count) - throws IOException { - checkNotClosed(); - checkOffsetAndCount(buffer.length, offset, count); - if (limit != -1 && content.size() > limit - count) { - throw new ProtocolException("exceeded content-length limit of " + limit + " bytes"); - } - content.write(buffer, offset, count); + @Override public synchronized void write(byte[] buffer, int offset, int count) + throws IOException { + checkNotClosed(); + checkOffsetAndCount(buffer.length, offset, count); + if (limit != -1 && content.size() > limit - count) { + throw new ProtocolException("exceeded content-length limit of " + limit + " bytes"); } + content.write(buffer, offset, count); + } - public synchronized int contentLength() throws IOException { - close(); - return content.size(); - } + public synchronized int contentLength() throws IOException { + close(); + return content.size(); + } - public void writeToSocket(OutputStream socketOut) throws IOException { - content.writeTo(socketOut); - } + public void writeToSocket(OutputStream socketOut) throws IOException { + content.writeTo(socketOut); + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java index 144344a9b..798cff3bc 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java +++ b/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java @@ -19,7 +19,6 @@ import com.squareup.okhttp.Address; import com.squareup.okhttp.Connection; import com.squareup.okhttp.ConnectionPool; import com.squareup.okhttp.internal.Dns; -import static com.squareup.okhttp.internal.Util.getEffectivePort; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -32,206 +31,206 @@ import java.util.Iterator; import java.util.List; import java.util.NoSuchElementException; +import static com.squareup.okhttp.internal.Util.getEffectivePort; + /** * Selects routes to connect to an origin server. Each connection requires a * choice of proxy server, IP address, and TLS mode. Connections may also be * recycled. */ public final class RouteSelector { - /** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */ - private static final int TLS_MODE_MODERN = 1; - /** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */ - private static final int TLS_MODE_COMPATIBLE = 0; - /** No TLS mode. */ - private static final int TLS_MODE_NULL = -1; + /** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */ + private static final int TLS_MODE_MODERN = 1; + /** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */ + private static final int TLS_MODE_COMPATIBLE = 0; + /** No TLS mode. */ + private static final int TLS_MODE_NULL = -1; - private final Address address; - private final URI uri; - private final ProxySelector proxySelector; - private final ConnectionPool pool; - private final Dns dns; + private final Address address; + private final URI uri; + private final ProxySelector proxySelector; + private final ConnectionPool pool; + private final Dns dns; - /* The most recently attempted route. */ - private Proxy lastProxy; - private InetSocketAddress lastInetSocketAddress; + /* The most recently attempted route. */ + private Proxy lastProxy; + private InetSocketAddress lastInetSocketAddress; - /* State for negotiating the next proxy to use. */ - private boolean hasNextProxy; - private Proxy userSpecifiedProxy; - private Iterator proxySelectorProxies; + /* State for negotiating the next proxy to use. */ + private boolean hasNextProxy; + private Proxy userSpecifiedProxy; + private Iterator proxySelectorProxies; - /* State for negotiating the next InetSocketAddress to use. */ - private InetAddress[] socketAddresses; - private int nextSocketAddressIndex; - private String socketHost; - private int socketPort; + /* State for negotiating the next InetSocketAddress to use. */ + private InetAddress[] socketAddresses; + private int nextSocketAddressIndex; + private String socketHost; + private int socketPort; - /* State for negotiating the next TLS configuration */ - private int nextTlsMode = TLS_MODE_NULL; + /* State for negotiating the next TLS configuration */ + private int nextTlsMode = TLS_MODE_NULL; - public RouteSelector(Address address, URI uri, ProxySelector proxySelector, - ConnectionPool pool, Dns dns) { - this.address = address; - this.uri = uri; - this.proxySelector = proxySelector; - this.pool = pool; - this.dns = dns; + public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool, + Dns dns) { + this.address = address; + this.uri = uri; + this.proxySelector = proxySelector; + this.pool = pool; + this.dns = dns; - resetNextProxy(uri, address.getProxy()); + resetNextProxy(uri, address.getProxy()); + } + + /** + * Returns true if there's another route to attempt. Every address has at + * least one route. + */ + public boolean hasNext() { + return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy(); + } + + /** + * Returns the next route address to attempt. + * + * @throws NoSuchElementException if there are no more routes to attempt. + */ + public Connection next() throws IOException { + // Always prefer pooled connections over new connections. + Connection pooled = pool.get(address); + if (pooled != null) { + return pooled; } - /** - * Returns true if there's another route to attempt. Every address has at - * least one route. - */ - public boolean hasNext() { - return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy(); - } - - /** - * Returns the next route address to attempt. - * - * @throws NoSuchElementException if there are no more routes to attempt. - */ - public Connection next() throws IOException { - // Always prefer pooled connections over new connections. - Connection pooled = pool.get(address); - if (pooled != null) { - return pooled; + // Compute the next route to attempt. + if (!hasNextTlsMode()) { + if (!hasNextInetSocketAddress()) { + if (!hasNextProxy()) { + throw new NoSuchElementException(); } + lastProxy = nextProxy(); + resetNextInetSocketAddress(lastProxy); + } + lastInetSocketAddress = nextInetSocketAddress(); + resetNextTlsMode(); + } + boolean modernTls = nextTlsMode() == TLS_MODE_MODERN; - // Compute the next route to attempt. - if (!hasNextTlsMode()) { - if (!hasNextInetSocketAddress()) { - if (!hasNextProxy()) { - throw new NoSuchElementException(); - } - lastProxy = nextProxy(); - resetNextInetSocketAddress(lastProxy); - } - lastInetSocketAddress = nextInetSocketAddress(); - resetNextTlsMode(); + return new Connection(address, lastProxy, lastInetSocketAddress, modernTls); + } + + /** + * Clients should invoke this method when they encounter a connectivity + * failure on a connection returned by this route selector. + */ + public void connectFailed(Connection connection, IOException failure) { + if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) { + // Tell the proxy selector when we fail to connect on a fresh connection. + proxySelector.connectFailed(uri, connection.getProxy().address(), failure); + } + } + + /** Resets {@link #nextProxy} to the first option. */ + private void resetNextProxy(URI uri, Proxy proxy) { + this.hasNextProxy = true; // This includes NO_PROXY! + if (proxy != null) { + this.userSpecifiedProxy = proxy; + } else { + List proxyList = proxySelector.select(uri); + if (proxyList != null) { + this.proxySelectorProxies = proxyList.iterator(); + } + } + } + + /** Returns true if there's another proxy to try. */ + private boolean hasNextProxy() { + return hasNextProxy; + } + + /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */ + private Proxy nextProxy() { + // If the user specifies a proxy, try that and only that. + if (userSpecifiedProxy != null) { + hasNextProxy = false; + return userSpecifiedProxy; + } + + // Try each of the ProxySelector choices until one connection succeeds. If none succeed + // then we'll try a direct connection below. + if (proxySelectorProxies != null) { + while (proxySelectorProxies.hasNext()) { + Proxy candidate = proxySelectorProxies.next(); + if (candidate.type() != Proxy.Type.DIRECT) { + return candidate; } - boolean modernTls = nextTlsMode() == TLS_MODE_MODERN; - - return new Connection(address, lastProxy, lastInetSocketAddress, modernTls); + } } - /** - * Clients should invoke this method when they encounter a connectivity - * failure on a connection returned by this route selector. - */ - public void connectFailed(Connection connection, IOException failure) { - if (connection.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) { - // Tell the proxy selector when we fail to connect on a fresh connection. - proxySelector.connectFailed(uri, connection.getProxy().address(), failure); - } + // Finally try a direct connection. + hasNextProxy = false; + return Proxy.NO_PROXY; + } + + /** Resets {@link #nextInetSocketAddress} to the first option. */ + private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException { + socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws! + + if (proxy.type() == Proxy.Type.DIRECT) { + socketHost = uri.getHost(); + socketPort = getEffectivePort(uri); + } else { + SocketAddress proxyAddress = proxy.address(); + if (!(proxyAddress instanceof InetSocketAddress)) { + throw new IllegalArgumentException( + "Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass()); + } + InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; + socketHost = proxySocketAddress.getHostName(); + socketPort = proxySocketAddress.getPort(); } - /** Resets {@link #nextProxy} to the first option. */ - private void resetNextProxy(URI uri, Proxy proxy) { - this.hasNextProxy = true; // This includes NO_PROXY! - if (proxy != null) { - this.userSpecifiedProxy = proxy; - } else { - List proxyList = proxySelector.select(uri); - if (proxyList != null) { - this.proxySelectorProxies = proxyList.iterator(); - } - } + // Try each address for best behavior in mixed IPv4/IPv6 environments. + socketAddresses = dns.getAllByName(socketHost); + nextSocketAddressIndex = 0; + } + + /** Returns true if there's another socket address to try. */ + private boolean hasNextInetSocketAddress() { + return socketAddresses != null; + } + + /** Returns the next socket address to try. */ + private InetSocketAddress nextInetSocketAddress() throws UnknownHostException { + InetSocketAddress result = + new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort); + if (nextSocketAddressIndex == socketAddresses.length) { + socketAddresses = null; // So that hasNextInetSocketAddress() returns false. + nextSocketAddressIndex = 0; } - /** Returns true if there's another proxy to try. */ - private boolean hasNextProxy() { - return hasNextProxy; - } - - /** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */ - private Proxy nextProxy() { - // If the user specifies a proxy, try that and only that. - if (userSpecifiedProxy != null) { - hasNextProxy = false; - return userSpecifiedProxy; - } - - // Try each of the ProxySelector choices until one connection succeeds. If none succeed - // then we'll try a direct connection below. - if (proxySelectorProxies != null) { - while (proxySelectorProxies.hasNext()) { - Proxy candidate = proxySelectorProxies.next(); - if (candidate.type() != Proxy.Type.DIRECT) { - return candidate; - } - } - } - - // Finally try a direct connection. - hasNextProxy = false; - return Proxy.NO_PROXY; - } - - /** Resets {@link #nextInetSocketAddress} to the first option. */ - private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException { - socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws! - - if (proxy.type() == Proxy.Type.DIRECT) { - socketHost = uri.getHost(); - socketPort = getEffectivePort(uri); - } else { - SocketAddress proxyAddress = proxy.address(); - if (!(proxyAddress instanceof InetSocketAddress)) { - throw new IllegalArgumentException("Proxy.address() is not an " - + "InetSocketAddress: " + proxyAddress.getClass()); - } - InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress; - socketHost = proxySocketAddress.getHostName(); - socketPort = proxySocketAddress.getPort(); - } - - // Try each address for best behavior in mixed IPv4/IPv6 environments. - socketAddresses = dns.getAllByName(socketHost); - nextSocketAddressIndex = 0; - } - - /** Returns true if there's another socket address to try. */ - private boolean hasNextInetSocketAddress() { - return socketAddresses != null; - } - - /** Returns the next socket address to try. */ - private InetSocketAddress nextInetSocketAddress() throws UnknownHostException { - InetSocketAddress result = new InetSocketAddress( - socketAddresses[nextSocketAddressIndex++], socketPort); - if (nextSocketAddressIndex == socketAddresses.length) { - socketAddresses = null; // So that hasNextInetSocketAddress() returns false. - nextSocketAddressIndex = 0; - } - - return result; - } - - /** Resets {@link #nextTlsMode} to the first option. */ - private void resetNextTlsMode() { - nextTlsMode = (address.getSslSocketFactory() != null) - ? TLS_MODE_MODERN - : TLS_MODE_COMPATIBLE; - } - - /** Returns true if there's another TLS mode to try. */ - private boolean hasNextTlsMode() { - return nextTlsMode != TLS_MODE_NULL; - } - - /** Returns the next TLS mode to try. */ - private int nextTlsMode() { - if (nextTlsMode == TLS_MODE_MODERN) { - nextTlsMode = TLS_MODE_COMPATIBLE; - return TLS_MODE_MODERN; - } else if (nextTlsMode == TLS_MODE_COMPATIBLE) { - nextTlsMode = TLS_MODE_NULL; // So that hasNextTlsMode() returns false. - return TLS_MODE_COMPATIBLE; - } else { - throw new AssertionError(); - } + return result; + } + + /** Resets {@link #nextTlsMode} to the first option. */ + private void resetNextTlsMode() { + nextTlsMode = (address.getSslSocketFactory() != null) ? TLS_MODE_MODERN : TLS_MODE_COMPATIBLE; + } + + /** Returns true if there's another TLS mode to try. */ + private boolean hasNextTlsMode() { + return nextTlsMode != TLS_MODE_NULL; + } + + /** Returns the next TLS mode to try. */ + private int nextTlsMode() { + if (nextTlsMode == TLS_MODE_MODERN) { + nextTlsMode = TLS_MODE_COMPATIBLE; + return TLS_MODE_MODERN; + } else if (nextTlsMode == TLS_MODE_COMPATIBLE) { + nextTlsMode = TLS_MODE_NULL; // So that hasNextTlsMode() returns false. + return TLS_MODE_COMPATIBLE; + } else { + throw new AssertionError(); } + } } 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 d96e9298c..193beb68c 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java +++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java @@ -26,64 +26,63 @@ import java.net.URL; import java.util.List; public final class SpdyTransport implements Transport { - private final HttpEngine httpEngine; - private final SpdyConnection spdyConnection; - private SpdyStream stream; + private final HttpEngine httpEngine; + private final SpdyConnection spdyConnection; + private SpdyStream stream; - public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) { - this.httpEngine = httpEngine; - this.spdyConnection = spdyConnection; - } + public SpdyTransport(HttpEngine httpEngine, SpdyConnection spdyConnection) { + this.httpEngine = httpEngine; + this.spdyConnection = spdyConnection; + } - @Override public OutputStream createRequestBody() throws IOException { - // TODO: if we aren't streaming up to the server, we should buffer the whole request - writeRequestHeaders(); - return stream.getOutputStream(); - } + @Override public OutputStream createRequestBody() throws IOException { + // TODO: if we aren't streaming up to the server, we should buffer the whole request + writeRequestHeaders(); + return stream.getOutputStream(); + } - @Override public void writeRequestHeaders() throws IOException { - if (stream != null) { - return; - } - httpEngine.writingRequestHeaders(); - RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders(); - String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0"; - 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(), - hasRequestBody, hasResponseBody); - stream.setReadTimeout(httpEngine.policy.getReadTimeout()); + @Override public void writeRequestHeaders() throws IOException { + if (stream != null) { + return; } + httpEngine.writingRequestHeaders(); + RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders(); + String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0"; + 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(), hasRequestBody, + hasResponseBody); + stream.setReadTimeout(httpEngine.policy.getReadTimeout()); + } - @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { - throw new UnsupportedOperationException(); - } + @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException { + throw new UnsupportedOperationException(); + } - @Override public void flushRequest() throws IOException { - stream.getOutputStream().close(); - } + @Override public void flushRequest() throws IOException { + stream.getOutputStream().close(); + } - @Override public ResponseHeaders readResponseHeaders() throws IOException { - List nameValueBlock = stream.getResponseHeaders(); - RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); - rawHeaders.computeResponseStatusLineFromSpdyHeaders(); - httpEngine.receiveHeaders(rawHeaders); - return new ResponseHeaders(httpEngine.uri, rawHeaders); - } + @Override public ResponseHeaders readResponseHeaders() throws IOException { + List nameValueBlock = stream.getResponseHeaders(); + RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock); + rawHeaders.computeResponseStatusLineFromSpdyHeaders(); + httpEngine.receiveHeaders(rawHeaders); + return new ResponseHeaders(httpEngine.uri, rawHeaders); + } - @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { - return new UnknownLengthHttpInputStream( - stream.getInputStream(), cacheRequest, httpEngine); - } + @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException { + return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine); + } - @Override public boolean makeReusable( - boolean streamCancelled, OutputStream requestBodyOut, InputStream responseBodyIn) { - if (streamCancelled) { - stream.closeLater(SpdyStream.RST_CANCEL); - } - return true; + @Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut, + InputStream responseBodyIn) { + if (streamCancelled) { + stream.closeLater(SpdyStream.RST_CANCEL); } + return true; + } } diff --git a/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/src/main/java/com/squareup/okhttp/internal/http/Transport.java index da9a58bc6..518827e88 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/Transport.java +++ b/src/main/java/com/squareup/okhttp/internal/http/Transport.java @@ -22,51 +22,43 @@ import java.io.OutputStream; import java.net.CacheRequest; interface Transport { - /** - * Returns an output stream where the request body can be written. The - * returned stream will of one of two types: - *

    - *
  • Direct. Bytes are written to the socket and - * forgotten. This is most efficient, particularly for large request - * bodies. The returned stream may be buffered; the caller must call - * {@link #flushRequest} before reading the response.
  • - *
  • Buffered. Bytes are written to an in memory - * buffer, and must be explicitly flushed with a call to {@link - * #writeRequestBody}. This allows HTTP authorization (401, 407) - * responses to be retransmitted transparently.
  • - *
- */ - // TODO: don't bother retransmitting the request body? It's quite a corner - // case and there's uncertainty whether Firefox or Chrome do this - OutputStream createRequestBody() throws IOException; + /** + * Returns an output stream where the request body can be written. The + * returned stream will of one of two types: + *
    + *
  • Direct. Bytes are written to the socket and + * forgotten. This is most efficient, particularly for large request + * bodies. The returned stream may be buffered; the caller must call + * {@link #flushRequest} before reading the response.
  • + *
  • Buffered. Bytes are written to an in memory + * buffer, and must be explicitly flushed with a call to {@link + * #writeRequestBody}. This allows HTTP authorization (401, 407) + * responses to be retransmitted transparently.
  • + *
+ */ + // TODO: don't bother retransmitting the request body? It's quite a corner + // case and there's uncertainty whether Firefox or Chrome do this + OutputStream createRequestBody() throws IOException; - /** - * This should update the HTTP engine's sentRequestMillis field. - */ - void writeRequestHeaders() throws IOException; + /** This should update the HTTP engine's sentRequestMillis field. */ + void writeRequestHeaders() throws IOException; - /** - * Sends the request body returned by {@link #createRequestBody} to the - * remote peer. - */ - void writeRequestBody(RetryableOutputStream requestBody) throws IOException; + /** + * Sends the request body returned by {@link #createRequestBody} to the + * remote peer. + */ + void writeRequestBody(RetryableOutputStream requestBody) throws IOException; - /** - * Flush the request body to the underlying socket. - */ - void flushRequest() throws IOException; + /** Flush the request body to the underlying socket. */ + void flushRequest() throws IOException; - /** - * Read response headers and update the cookie manager. - */ - ResponseHeaders readResponseHeaders() throws IOException; + /** Read response headers and update the cookie manager. */ + ResponseHeaders readResponseHeaders() throws IOException; - // TODO: make this the content stream? - InputStream getTransferStream(CacheRequest cacheRequest) throws IOException; + // TODO: make this the content stream? + InputStream getTransferStream(CacheRequest cacheRequest) throws IOException; - /** - * Returns true if the underlying connection can be recycled. - */ - boolean makeReusable(boolean streamReusable, - OutputStream requestBodyOut, InputStream responseBodyIn); + /** Returns true if the underlying connection can be recycled. */ + boolean makeReusable(boolean streamReusable, OutputStream requestBodyOut, + InputStream responseBodyIn); } diff --git a/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java b/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java index 03ac12d5f..729e0b92d 100644 --- a/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java +++ b/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java @@ -15,50 +15,49 @@ */ package com.squareup.okhttp.internal.http; -import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; import java.io.IOException; import java.io.InputStream; import java.net.CacheRequest; -/** - * An HTTP message body terminated by the end of the underlying stream. - */ +import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; + +/** An HTTP message body terminated by the end of the underlying stream. */ final class UnknownLengthHttpInputStream extends AbstractHttpInputStream { - private boolean inputExhausted; + private boolean inputExhausted; - UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, - HttpEngine httpEngine) throws IOException { - super(is, httpEngine, cacheRequest); - } + UnknownLengthHttpInputStream(InputStream is, CacheRequest cacheRequest, HttpEngine httpEngine) + throws IOException { + super(is, httpEngine, cacheRequest); + } - @Override public int read(byte[] buffer, int offset, int count) throws IOException { - checkOffsetAndCount(buffer.length, offset, count); - checkNotClosed(); - if (in == null || inputExhausted) { - return -1; - } - int read = in.read(buffer, offset, count); - if (read == -1) { - inputExhausted = true; - endOfInput(false); - return -1; - } - cacheWrite(buffer, offset, read); - return read; + @Override public int read(byte[] buffer, int offset, int count) throws IOException { + checkOffsetAndCount(buffer.length, offset, count); + checkNotClosed(); + if (in == null || inputExhausted) { + return -1; } + int read = in.read(buffer, offset, count); + if (read == -1) { + inputExhausted = true; + endOfInput(false); + return -1; + } + cacheWrite(buffer, offset, read); + return read; + } - @Override public int available() throws IOException { - checkNotClosed(); - return in == null ? 0 : in.available(); - } + @Override public int available() throws IOException { + checkNotClosed(); + return in == null ? 0 : in.available(); + } - @Override public void close() throws IOException { - if (closed) { - return; - } - closed = true; - if (!inputExhausted) { - unexpectedEndOfInput(); - } + @Override public void close() throws IOException { + if (closed) { + return; } + closed = true; + if (!inputExhausted) { + unexpectedEndOfInput(); + } + } } diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java b/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java index fc554f410..875fff0fd 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/IncomingStreamHandler.java @@ -18,21 +18,19 @@ package com.squareup.okhttp.internal.spdy; import java.io.IOException; -/** - * Listener to be notified when a connected peer creates a new stream. - */ +/** Listener to be notified when a connected peer creates a new stream. */ public interface IncomingStreamHandler { - IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() { - @Override public void receive(SpdyStream stream) throws IOException { - stream.close(SpdyStream.RST_REFUSED_STREAM); - } - }; + IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + stream.close(SpdyStream.RST_REFUSED_STREAM); + } + }; - /** - * Handle a new stream from this connection's peer. Implementations should - * respond by either {@link SpdyStream#reply replying to the stream} or - * {@link SpdyStream#close closing it}. This response does not need to be - * synchronous. - */ - void receive(SpdyStream stream) throws IOException; + /** + * Handle a new stream from this connection's peer. Implementations should + * respond by either {@link SpdyStream#reply replying to the stream} or + * {@link SpdyStream#close closing it}. This response does not need to be + * synchronous. + */ + void receive(SpdyStream stream) throws IOException; } diff --git a/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java b/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java index 1fc3979c2..c58525578 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java @@ -22,50 +22,50 @@ import java.util.concurrent.TimeUnit; * A locally-originated ping. */ public final class Ping { - private final CountDownLatch latch = new CountDownLatch(1); - private long sent = -1; - private long received = -1; + private final CountDownLatch latch = new CountDownLatch(1); + private long sent = -1; + private long received = -1; - Ping() { - } + Ping() { + } - void send() { - if (sent != -1) throw new IllegalStateException(); - sent = System.nanoTime(); - } + void send() { + if (sent != -1) throw new IllegalStateException(); + sent = System.nanoTime(); + } - void receive() { - if (received != -1 || sent == -1) throw new IllegalStateException(); - received = System.nanoTime(); - latch.countDown(); - } + void receive() { + if (received != -1 || sent == -1) throw new IllegalStateException(); + received = System.nanoTime(); + latch.countDown(); + } - void cancel() { - if (received != -1 || sent == -1) throw new IllegalStateException(); - received = sent - 1; - latch.countDown(); - } + void cancel() { + if (received != -1 || sent == -1) throw new IllegalStateException(); + received = sent - 1; + latch.countDown(); + } - /** - * Returns the round trip time for this ping in nanoseconds, waiting for the - * response to arrive if necessary. Returns -1 if the response was - * cancelled. - */ - public long roundTripTime() throws InterruptedException { - latch.await(); - return received - sent; - } + /** + * Returns the round trip time for this ping in nanoseconds, waiting for the + * response to arrive if necessary. Returns -1 if the response was + * cancelled. + */ + public long roundTripTime() throws InterruptedException { + latch.await(); + return received - sent; + } - /** - * Returns the round trip time for this ping in nanoseconds, or -1 if the - * response was cancelled, or -2 if the timeout elapsed before the round - * trip completed. - */ - public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException { - if (latch.await(timeout, unit)) { - return received - sent; - } else { - return -2; - } + /** + * Returns the round trip time for this ping in nanoseconds, or -1 if the + * response was cancelled, or -2 if the timeout elapsed before the round + * trip completed. + */ + public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException { + if (latch.await(timeout, unit)) { + return received - sent; + } else { + return -2; } + } } 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 ad847a848..774d79121 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java @@ -16,169 +16,159 @@ package com.squareup.okhttp.internal.spdy; final class Settings { - /** - * From the spdy/3 spec, the default initial window size for all streams is - * 64 KiB. (Chrome 25 uses 10 MiB). - */ - static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024; + /** + * From the spdy/3 spec, the default initial window size for all streams is + * 64 KiB. (Chrome 25 uses 10 MiB). + */ + static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024; - /** Peer request to clear durable settings. */ - static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1; + /** Peer request to clear durable settings. */ + static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1; - /** Sent by servers only. The peer requests this setting persisted for future connections. */ - static final int PERSIST_VALUE = 0x1; - /** Sent by clients only. The client is reminding the server of a persisted value. */ - static final int PERSISTED = 0x2; + /** Sent by servers only. The peer requests this setting persisted for future connections. */ + static final int PERSIST_VALUE = 0x1; + /** Sent by clients only. The client is reminding the server of a persisted value. */ + static final int PERSISTED = 0x2; - /** Sender's estimate of max incoming kbps. */ - static final int UPLOAD_BANDWIDTH = 0x1; - /** Sender's estimate of max outgoing kbps. */ - static final int DOWNLOAD_BANDWIDTH = 0x2; - /** Sender's estimate of milliseconds between sending a request and receiving a response. */ - static final int ROUND_TRIP_TIME = 0x3; - /** Sender's maximum number of concurrent streams. */ - static final int MAX_CONCURRENT_STREAMS = 0x4; - /** Current CWND in Packets. */ - static final int CURRENT_CWND = 0x5; - /** Retransmission rate. Percentage */ - 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 = 0x9; + /** Sender's estimate of max incoming kbps. */ + static final int UPLOAD_BANDWIDTH = 0x1; + /** Sender's estimate of max outgoing kbps. */ + static final int DOWNLOAD_BANDWIDTH = 0x2; + /** Sender's estimate of milliseconds between sending a request and receiving a response. */ + static final int ROUND_TRIP_TIME = 0x3; + /** Sender's maximum number of concurrent streams. */ + static final int MAX_CONCURRENT_STREAMS = 0x4; + /** Current CWND in Packets. */ + static final int CURRENT_CWND = 0x5; + /** Retransmission rate. Percentage */ + 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 = 0x9; - /** Bitfield of which flags that values. */ - private int set; + /** Bitfield of which flags that values. */ + private int set; - /** Bitfield of flags that have {@link #PERSIST_VALUE}. */ - private int persistValue; + /** Bitfield of flags that have {@link #PERSIST_VALUE}. */ + private int persistValue; - /** Bitfield of flags that have {@link #PERSISTED}. */ - private int persisted; + /** Bitfield of flags that have {@link #PERSISTED}. */ + private int persisted; - /** Flag values. */ - private final int[] values = new int[COUNT]; + /** Flag values. */ + private final int[] values = new int[COUNT]; - void set(int id, int idFlags, int value) { - if (id >= values.length) { - return; // Discard unknown settings. - } - - int bit = 1 << id; - set |= bit; - if ((idFlags & PERSIST_VALUE) != 0) { - persistValue |= bit; - } else { - persistValue &= ~bit; - } - if ((idFlags & PERSISTED) != 0) { - persisted |= bit; - } else { - persisted &= ~bit; - } - - values[id] = value; + void set(int id, int idFlags, int value) { + if (id >= values.length) { + return; // Discard unknown settings. } - /** - * Returns true if a value has been assigned for the setting {@code id}. - */ - boolean isSet(int id) { - int bit = 1 << id; - return (set & bit) != 0; + int bit = 1 << id; + set |= bit; + if ((idFlags & PERSIST_VALUE) != 0) { + persistValue |= bit; + } else { + persistValue &= ~bit; + } + if ((idFlags & PERSISTED) != 0) { + persisted |= bit; + } else { + persisted &= ~bit; } - /** - * Returns the value for the setting {@code id}, or 0 if unset. - */ - int get(int id) { - return values[id]; - } + values[id] = value; + } - /** - * Returns the flags for the setting {@code id}, or 0 if unset. - */ - int flags(int id) { - int result = 0; - if (isPersisted(id)) result |= Settings.PERSISTED; - if (persistValue(id)) result |= Settings.PERSIST_VALUE; - return result; - } + /** Returns true if a value has been assigned for the setting {@code id}. */ + boolean isSet(int id) { + int bit = 1 << id; + return (set & bit) != 0; + } - /** - * Returns the number of settings that have values assigned. - */ - int size() { - return Integer.bitCount(set); - } + /** Returns the value for the setting {@code id}, or 0 if unset. */ + int get(int id) { + return values[id]; + } - int getUploadBandwidth(int defaultValue) { - int bit = 1 << UPLOAD_BANDWIDTH; - return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue; - } + /** Returns the flags for the setting {@code id}, or 0 if unset. */ + int flags(int id) { + int result = 0; + if (isPersisted(id)) result |= Settings.PERSISTED; + if (persistValue(id)) result |= Settings.PERSIST_VALUE; + return result; + } - int getDownloadBandwidth(int defaultValue) { - int bit = 1 << DOWNLOAD_BANDWIDTH; - return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue; - } + /** Returns the number of settings that have values assigned. */ + int size() { + return Integer.bitCount(set); + } - int getRoundTripTime(int defaultValue) { - int bit = 1 << ROUND_TRIP_TIME; - return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue; - } + int getUploadBandwidth(int defaultValue) { + int bit = 1 << UPLOAD_BANDWIDTH; + return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue; + } - int getMaxConcurrentStreams(int defaultValue) { - int bit = 1 << MAX_CONCURRENT_STREAMS; - return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; - } + int getDownloadBandwidth(int defaultValue) { + int bit = 1 << DOWNLOAD_BANDWIDTH; + return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue; + } - int getCurrentCwnd(int defaultValue) { - int bit = 1 << CURRENT_CWND; - return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue; - } + int getRoundTripTime(int defaultValue) { + int bit = 1 << ROUND_TRIP_TIME; + return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue; + } - int getDownloadRetransRate(int defaultValue) { - int bit = 1 << DOWNLOAD_RETRANS_RATE; - return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue; - } + int getMaxConcurrentStreams(int defaultValue) { + int bit = 1 << MAX_CONCURRENT_STREAMS; + return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue; + } - int getInitialWindowSize(int defaultValue) { - int bit = 1 << INITIAL_WINDOW_SIZE; - return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue; - } + int getCurrentCwnd(int defaultValue) { + int bit = 1 << CURRENT_CWND; + return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue; + } - int getClientCertificateVectorSize(int defaultValue) { - int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE; - return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; - } + int getDownloadRetransRate(int defaultValue) { + int bit = 1 << DOWNLOAD_RETRANS_RATE; + return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue; + } - /** - * Returns true if this user agent should use this setting in future SPDY - * connections to the same host. - */ - boolean persistValue(int id) { - int bit = 1 << id; - return (persistValue & bit) != 0; - } + int getInitialWindowSize(int defaultValue) { + int bit = 1 << INITIAL_WINDOW_SIZE; + return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue; + } - /** - * Returns true if this setting was persisted. - */ - boolean isPersisted(int id) { - int bit = 1 << id; - return (persisted & bit) != 0; - } + int getClientCertificateVectorSize(int defaultValue) { + int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE; + return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue; + } - /** - * Writes {@code other} into this. If any setting is populated by this and - * {@code other}, the value and flags from {@code other} will be kept. - */ - void merge(Settings other) { - for (int i = 0; i < COUNT; i++) { - if (!other.isSet(i)) continue; - set(i, other.flags(i), other.get(i)); - } + /** + * Returns true if this user agent should use this setting in future SPDY + * connections to the same host. + */ + boolean persistValue(int id) { + int bit = 1 << id; + return (persistValue & bit) != 0; + } + + /** Returns true if this setting was persisted. */ + boolean isPersisted(int id) { + int bit = 1 << id; + return (persisted & bit) != 0; + } + + /** + * Writes {@code other} into this. If any setting is populated by this and + * {@code other}, the value and flags from {@code other} will be kept. + */ + void merge(Settings other) { + for (int i = 0; i < COUNT; i++) { + if (!other.isSet(i)) continue; + set(i, other.flags(i), other.get(i)); } + } } 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 70228dd1d..4082f9140 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java @@ -57,536 +57,529 @@ public final class SpdyConnection implements Closeable { * wait for a blocking operation while holding 'this'. */ - static final int FLAG_FIN = 0x1; - static final int FLAG_UNIDIRECTIONAL = 0x2; + static final int FLAG_FIN = 0x1; + static final int FLAG_UNIDIRECTIONAL = 0x2; - static final int TYPE_DATA = 0x0; - static final int TYPE_SYN_STREAM = 0x1; - static final int TYPE_SYN_REPLY = 0x2; - static final int TYPE_RST_STREAM = 0x3; - static final int TYPE_SETTINGS = 0x4; - static final int TYPE_NOOP = 0x5; - static final int TYPE_PING = 0x6; - static final int TYPE_GOAWAY = 0x7; - static final int TYPE_HEADERS = 0x8; - static final int TYPE_WINDOW_UPDATE = 0x9; - static final int TYPE_CREDENTIAL = 0x10; - static final int VERSION = 3; + static final int TYPE_DATA = 0x0; + static final int TYPE_SYN_STREAM = 0x1; + static final int TYPE_SYN_REPLY = 0x2; + static final int TYPE_RST_STREAM = 0x3; + static final int TYPE_SETTINGS = 0x4; + static final int TYPE_NOOP = 0x5; + static final int TYPE_PING = 0x6; + static final int TYPE_GOAWAY = 0x7; + static final int TYPE_HEADERS = 0x8; + 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; + 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. - */ - final boolean client; + /** True if this peer initiated the connection. */ + final boolean client; - /** - * User code to run in response to an incoming stream. Callbacks must not be - * run on the callback executor. - */ - private final IncomingStreamHandler handler; + /** + * User code to run in response to an incoming stream. Callbacks must not be + * run on the callback executor. + */ + private final IncomingStreamHandler handler; - private final SpdyReader spdyReader; - private final SpdyWriter spdyWriter; - private final ExecutorService readExecutor; - private final ExecutorService writeExecutor; - private final ExecutorService callbackExecutor; + private final SpdyReader spdyReader; + private final SpdyWriter spdyWriter; + private final ExecutorService readExecutor; + private final ExecutorService writeExecutor; + private final ExecutorService callbackExecutor; - private final Map streams = new HashMap(); - private int lastGoodStreamId; - private int nextStreamId; - private boolean shutdown; - private long idleStartTimeNs = System.nanoTime(); + private final Map streams = new HashMap(); + private int lastGoodStreamId; + private int nextStreamId; + private boolean shutdown; + private long idleStartTimeNs = System.nanoTime(); - /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */ - private Map pings; - private int nextPingId; + /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */ + private Map pings; + private int nextPingId; - /** Lazily-created settings for this connection. */ - Settings settings; + /** Lazily-created settings for this connection. */ + Settings settings; - private SpdyConnection(Builder builder) { - client = builder.client; - handler = builder.handler; - spdyReader = new SpdyReader(builder.in); - spdyWriter = new SpdyWriter(builder.out); - nextStreamId = builder.client ? 1 : 2; - nextPingId = builder.client ? 1 : 2; + private SpdyConnection(Builder builder) { + client = builder.client; + handler = builder.handler; + spdyReader = new SpdyReader(builder.in); + spdyWriter = new SpdyWriter(builder.out); + nextStreamId = builder.client ? 1 : 2; + nextPingId = builder.client ? 1 : 2; - String prefix = builder.client ? "Spdy Client " : "Spdy Server "; - readExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, - new SynchronousQueue(), Util.newThreadFactory(prefix + "Reader", false)); - writeExecutor = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, - new LinkedBlockingQueue(), Util.newThreadFactory(prefix + "Writer", false)); - callbackExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, - new SynchronousQueue(), Util.newThreadFactory(prefix + "Callbacks", false)); + String prefix = builder.client ? "Spdy Client " : "Spdy Server "; + readExecutor = + new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new SynchronousQueue(), + Util.newThreadFactory(prefix + "Reader", false)); + writeExecutor = + new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue(), + Util.newThreadFactory(prefix + "Writer", false)); + callbackExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, + new SynchronousQueue(), Util.newThreadFactory(prefix + "Callbacks", false)); - readExecutor.execute(new Reader()); + readExecutor.execute(new Reader()); + } + + /** + * Returns the number of {@link SpdyStream#isOpen() open streams} on this + * connection. + */ + public synchronized int openStreamCount() { + return streams.size(); + } + + private synchronized SpdyStream getStream(int id) { + return streams.get(id); + } + + synchronized SpdyStream removeStream(int streamId) { + SpdyStream stream = streams.remove(streamId); + if (stream != null && streams.isEmpty()) { + setIdle(true); } + return stream; + } - /** - * Returns the number of {@link SpdyStream#isOpen() open streams} on this - * connection. - */ - public synchronized int openStreamCount() { - return streams.size(); - } + private void setIdle(boolean value) { + idleStartTimeNs = value ? System.nanoTime() : 0L; + } - private synchronized SpdyStream getStream(int id) { - return streams.get(id); - } + /** Returns true if this connection is idle. */ + public boolean isIdle() { + return idleStartTimeNs != 0L; + } - synchronized SpdyStream removeStream(int streamId) { - SpdyStream stream = streams.remove(streamId); - if (stream != null && streams.isEmpty()) { - setIdle(true); + /** Returns the time in ns when this connection became idle or 0L if connection is not idle. */ + public long getIdleStartTimeNs() { + return idleStartTimeNs; + } + + /** + * Returns a new locally-initiated stream. + * + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + * @param in true to create an input stream that the remote peer can use to + * send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}. + */ + 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 slot = 0; // TODO: permit the caller to specify a slot? + SpdyStream stream; + int streamId; + + synchronized (spdyWriter) { + synchronized (this) { + if (shutdown) { + throw new IOException("shutdown"); + } + streamId = nextStreamId; + nextStreamId += 2; + stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders, settings); + if (stream.isOpen()) { + streams.put(streamId, stream); + setIdle(false); + } } - return stream; + + spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, requestHeaders); } - private void setIdle(boolean value) { - idleStartTimeNs = value ? System.nanoTime() : 0L; + return stream; + } + + void writeSynReply(int streamId, int flags, List alternating) throws IOException { + 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); } + } - /** - * Returns true if this connection is idle. - */ - public boolean isIdle() { - return idleStartTimeNs != 0L; - } - - /** - * Returns the time in ns when this connection became idle or 0L if connection is not idle. - */ - public long getIdleStartTimeNs() { - return idleStartTimeNs; - } - - /** - * Returns a new locally-initiated stream. - * - * @param out true to create an output stream that we can use to send data - * to the remote peer. Corresponds to {@code FLAG_FIN}. - * @param in true to create an input stream that the remote peer can use to - * send data to us. Corresponds to {@code FLAG_UNIDIRECTIONAL}. - */ - 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 slot = 0; // TODO: permit the caller to specify a slot? - SpdyStream stream; - int streamId; - - synchronized (spdyWriter) { - synchronized (this) { - if (shutdown) { - throw new IOException("shutdown"); - } - streamId = nextStreamId; - nextStreamId += 2; - stream = new SpdyStream(streamId, this, flags, priority, slot, requestHeaders, - settings); - if (stream.isOpen()) { - streams.put(streamId, stream); - setIdle(false); - } - } - - spdyWriter.synStream(flags, streamId, associatedStreamId, priority, slot, - requestHeaders); - } - - return stream; - } - - void writeSynReply(int streamId, int flags, List alternating) throws IOException { - 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); - } - } - - void writeSynResetLater(final int streamId, final int statusCode) { - writeExecutor.execute(new Runnable() { - @Override public void run() { - try { - writeSynReset(streamId, statusCode); - } catch (IOException ignored) { - } - } - }); - } - - void writeSynReset(int streamId, int statusCode) throws IOException { - spdyWriter.rstStream(streamId, statusCode); - } - - void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) { - writeExecutor.execute(new Runnable() { - @Override public void run() { - try { - writeWindowUpdate(streamId, deltaWindowSize); - } catch (IOException ignored) { - } - } - }); - } - - void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException { - spdyWriter.windowUpdate(streamId, deltaWindowSize); - } - - /** - * Sends a ping frame to the peer. Use the returned object to await the - * ping's response and observe its round trip time. - */ - public Ping ping() throws IOException { - Ping ping = new Ping(); - int pingId; - synchronized (this) { - if (shutdown) { - throw new IOException("shutdown"); - } - pingId = nextPingId; - nextPingId += 2; - if (pings == null) pings = new HashMap(); - pings.put(pingId, ping); - } - writePing(pingId, ping); - return ping; - } - - private void writePingLater(final int id, final Ping ping) { - writeExecutor.execute(new Runnable() { - @Override public void run() { - try { - writePing(id, ping); - } catch (IOException ignored) { - } - } - }); - } - - private void writePing(int id, Ping ping) throws IOException { - synchronized (spdyWriter) { - // Observe the sent time immediately before performing I/O. - if (ping != null) ping.send(); - spdyWriter.ping(0, id); - } - } - - private synchronized Ping removePing(int id) { - return pings != null ? pings.remove(id) : null; - } - - /** - * Sends a noop frame to the peer. - */ - public void noop() throws IOException { - spdyWriter.noop(); - } - - public void flush() throws IOException { - synchronized (spdyWriter) { - spdyWriter.out.flush(); - } - } - - 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(int statusCode) throws IOException { - synchronized (spdyWriter) { - int lastGoodStreamId; - synchronized (this) { - if (shutdown) { - return; - } - shutdown = true; - lastGoodStreamId = this.lastGoodStreamId; - } - spdyWriter.goAway(0, lastGoodStreamId, statusCode); - } - } - - /** - * Closes this connection. This cancels all open streams and unanswered - * pings. It closes the underlying input and output streams and shuts down - * internal executor services. - */ - @Override public void close() throws IOException { - close(GOAWAY_OK, SpdyStream.RST_CANCEL); - } - - private void close(int shutdownStatusCode, int rstStatusCode) throws IOException { - assert (!Thread.holdsLock(this)); - IOException thrown = null; + void writeSynResetLater(final int streamId, final int statusCode) { + writeExecutor.execute(new Runnable() { + @Override public void run() { try { - shutdown(shutdownStatusCode); - } catch (IOException e) { - thrown = e; + writeSynReset(streamId, statusCode); + } catch (IOException ignored) { } + } + }); + } - SpdyStream[] streamsToClose = null; - Ping[] pingsToCancel = null; - synchronized (this) { - if (!streams.isEmpty()) { - streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]); - streams.clear(); - setIdle(false); - } - if (pings != null) { - pingsToCancel = pings.values().toArray(new Ping[pings.size()]); - pings = null; - } - } + void writeSynReset(int streamId, int statusCode) throws IOException { + spdyWriter.rstStream(streamId, statusCode); + } - if (streamsToClose != null) { - for (SpdyStream stream : streamsToClose) { - try { - stream.close(rstStatusCode); - } catch (IOException e) { - if (thrown != null) thrown = e; - } - } - } - - if (pingsToCancel != null) { - for (Ping ping : pingsToCancel) { - ping.cancel(); - } - } - - writeExecutor.shutdown(); - callbackExecutor.shutdown(); - readExecutor.shutdown(); + void writeWindowUpdateLater(final int streamId, final int deltaWindowSize) { + writeExecutor.execute(new Runnable() { + @Override public void run() { try { - spdyReader.close(); - } catch (IOException e) { - thrown = e; + writeWindowUpdate(streamId, deltaWindowSize); + } catch (IOException ignored) { } + } + }); + } + + void writeWindowUpdate(int streamId, int deltaWindowSize) throws IOException { + spdyWriter.windowUpdate(streamId, deltaWindowSize); + } + + /** + * Sends a ping frame to the peer. Use the returned object to await the + * ping's response and observe its round trip time. + */ + public Ping ping() throws IOException { + Ping ping = new Ping(); + int pingId; + synchronized (this) { + if (shutdown) { + throw new IOException("shutdown"); + } + pingId = nextPingId; + nextPingId += 2; + if (pings == null) pings = new HashMap(); + pings.put(pingId, ping); + } + writePing(pingId, ping); + return ping; + } + + private void writePingLater(final int id, final Ping ping) { + writeExecutor.execute(new Runnable() { + @Override public void run() { try { - spdyWriter.close(); - } catch (IOException e) { - if (thrown == null) thrown = e; + writePing(id, ping); + } catch (IOException ignored) { } - if (thrown != null) throw thrown; + } + }); + } + + private void writePing(int id, Ping ping) throws IOException { + synchronized (spdyWriter) { + // Observe the sent time immediately before performing I/O. + if (ping != null) ping.send(); + spdyWriter.ping(0, id); + } + } + + private synchronized Ping removePing(int id) { + return pings != null ? pings.remove(id) : null; + } + + /** Sends a noop frame to the peer. */ + public void noop() throws IOException { + spdyWriter.noop(); + } + + public void flush() throws IOException { + synchronized (spdyWriter) { + spdyWriter.out.flush(); + } + } + + 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(int statusCode) throws IOException { + synchronized (spdyWriter) { + int lastGoodStreamId; + synchronized (this) { + if (shutdown) { + return; + } + shutdown = true; + lastGoodStreamId = this.lastGoodStreamId; + } + spdyWriter.goAway(0, lastGoodStreamId, statusCode); + } + } + + /** + * Closes this connection. This cancels all open streams and unanswered + * pings. It closes the underlying input and output streams and shuts down + * internal executor services. + */ + @Override public void close() throws IOException { + close(GOAWAY_OK, SpdyStream.RST_CANCEL); + } + + private void close(int shutdownStatusCode, int rstStatusCode) throws IOException { + assert (!Thread.holdsLock(this)); + IOException thrown = null; + try { + shutdown(shutdownStatusCode); + } catch (IOException e) { + thrown = e; } - public static class Builder { - private InputStream in; - private OutputStream out; - private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; - public boolean client; - - /** - * @param client true if this peer initiated the connection; false if - * this peer accepted the connection. - */ - public Builder(boolean client, Socket socket) throws IOException { - this(client, socket.getInputStream(), socket.getOutputStream()); - } - - /** - * @param client true if this peer initiated the connection; false if this - * peer accepted the connection. - */ - public Builder(boolean client, InputStream in, OutputStream out) { - this.client = client; - this.in = in; - this.out = out; - } - - public Builder handler(IncomingStreamHandler handler) { - this.handler = handler; - return this; - } - - public SpdyConnection build() { - return new SpdyConnection(this); - } + SpdyStream[] streamsToClose = null; + Ping[] pingsToCancel = null; + synchronized (this) { + if (!streams.isEmpty()) { + streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]); + streams.clear(); + setIdle(false); + } + if (pings != null) { + pingsToCancel = pings.values().toArray(new Ping[pings.size()]); + pings = null; + } } - private class Reader implements Runnable, SpdyReader.Handler { + if (streamsToClose != null) { + for (SpdyStream stream : streamsToClose) { + try { + stream.close(rstStatusCode); + } catch (IOException e) { + if (thrown != null) thrown = e; + } + } + } + + if (pingsToCancel != null) { + for (Ping ping : pingsToCancel) { + ping.cancel(); + } + } + + writeExecutor.shutdown(); + callbackExecutor.shutdown(); + readExecutor.shutdown(); + try { + spdyReader.close(); + } catch (IOException e) { + thrown = e; + } + try { + spdyWriter.close(); + } catch (IOException e) { + if (thrown == null) thrown = e; + } + if (thrown != null) throw thrown; + } + + public static class Builder { + private InputStream in; + private OutputStream out; + private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS; + public boolean client; + + /** + * @param client true if this peer initiated the connection; false if + * this peer accepted the connection. + */ + public Builder(boolean client, Socket socket) throws IOException { + this(client, socket.getInputStream(), socket.getOutputStream()); + } + + /** + * @param client true if this peer initiated the connection; false if this + * peer accepted the connection. + */ + public Builder(boolean client, InputStream in, OutputStream out) { + this.client = client; + this.in = in; + this.out = out; + } + + public Builder handler(IncomingStreamHandler handler) { + this.handler = handler; + return this; + } + + public SpdyConnection build() { + return new SpdyConnection(this); + } + } + + private class Reader implements Runnable, SpdyReader.Handler { + @Override public void run() { + int shutdownStatusCode = GOAWAY_INTERNAL_ERROR; + int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR; + try { + while (spdyReader.nextFrame(this)) { + } + shutdownStatusCode = GOAWAY_OK; + rstStatusCode = SpdyStream.RST_CANCEL; + } catch (IOException e) { + shutdownStatusCode = GOAWAY_PROTOCOL_ERROR; + rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR; + } finally { + try { + close(shutdownStatusCode, rstStatusCode); + } catch (IOException ignored) { + } + } + } + + @Override public void data(int flags, int streamId, InputStream in, int length) + throws IOException { + SpdyStream dataStream = getStream(streamId); + if (dataStream == null) { + writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM); + Util.skipByReading(in, length); + return; + } + dataStream.receiveData(in, length); + if ((flags & SpdyConnection.FLAG_FIN) != 0) { + dataStream.receiveFin(); + } + } + + @Override + public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot, + List nameValueBlock) { + final SpdyStream synStream; + final SpdyStream previous; + synchronized (SpdyConnection.this) { + synStream = + new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, nameValueBlock, + settings); + if (shutdown) { + return; + } + lastGoodStreamId = streamId; + previous = streams.put(streamId, synStream); + } + if (previous != null) { + previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR); + removeStream(streamId); + return; + } + callbackExecutor.execute(new Runnable() { @Override public void run() { - int shutdownStatusCode = GOAWAY_INTERNAL_ERROR; - int rstStatusCode = SpdyStream.RST_INTERNAL_ERROR; - try { - while (spdyReader.nextFrame(this)) { - } - shutdownStatusCode = GOAWAY_OK; - rstStatusCode = SpdyStream.RST_CANCEL; - } catch (IOException e) { - shutdownStatusCode = GOAWAY_PROTOCOL_ERROR; - rstStatusCode = SpdyStream.RST_PROTOCOL_ERROR; - } finally { - try { - close(shutdownStatusCode, rstStatusCode); - } catch (IOException ignored) { - } - } - } - - @Override public void data(int flags, int streamId, InputStream in, int length) - throws IOException { - SpdyStream dataStream = getStream(streamId); - if (dataStream == null) { - writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM); - Util.skipByReading(in, length); - return; - } - dataStream.receiveData(in, length); - if ((flags & SpdyConnection.FLAG_FIN) != 0) { - dataStream.receiveFin(); - } - } - - @Override public void synStream(int flags, int streamId, int associatedStreamId, - int priority, int slot, List nameValueBlock) { - final SpdyStream synStream; - final SpdyStream previous; - synchronized (SpdyConnection.this) { - synStream = new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, - nameValueBlock, settings); - if (shutdown) { - return; - } - lastGoodStreamId = streamId; - previous = streams.put(streamId, synStream); - } - if (previous != null) { - previous.closeLater(SpdyStream.RST_PROTOCOL_ERROR); - removeStream(streamId); - return; - } - callbackExecutor.execute(new Runnable() { - @Override public void run() { - try { - handler.receive(synStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); - } - - @Override public void synReply(int flags, int streamId, List nameValueBlock) - throws IOException { - SpdyStream replyStream = getStream(streamId); - if (replyStream == null) { - writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM); - return; - } - replyStream.receiveReply(nameValueBlock); - if ((flags & SpdyConnection.FLAG_FIN) != 0) { - replyStream.receiveFin(); - } - } - - @Override public void headers(int flags, int streamId, List nameValueBlock) - throws IOException { - SpdyStream replyStream = getStream(streamId); - if (replyStream != null) { - replyStream.receiveHeaders(nameValueBlock); - } - } - - @Override public void rstStream(int flags, int streamId, int statusCode) { - SpdyStream rstStream = removeStream(streamId); - if (rstStream != null) { - rstStream.receiveRstStream(statusCode); - } - } - - @Override public void settings(int flags, Settings newSettings) { - SpdyStream[] streamsToNotify = null; - synchronized (SpdyConnection.this) { - if (settings == null - || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) { - settings = newSettings; - } else { - settings.merge(newSettings); - } - if (!streams.isEmpty()) { - streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]); - } - } - if (streamsToNotify != null) { - for (SpdyStream stream : streamsToNotify) { - // The synchronization here is ugly. We need to synchronize on 'this' to guard - // reads to 'settings'. We synchronize on 'stream' to guard the state change. - // And we need to acquire the 'stream' lock first, since that may block. - synchronized (stream) { - synchronized (this) { - stream.receiveSettings(settings); - } - } - } - } - } - - @Override public void noop() { - } - - @Override public void ping(int flags, int streamId) { - if (client != (streamId % 2 == 1)) { - // Respond to a client ping if this is a server and vice versa. - writePingLater(streamId, null); - } else { - Ping ping = removePing(streamId); - if (ping != null) { - ping.receive(); - } - } - } - - @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) { - synchronized (SpdyConnection.this) { - shutdown = true; - - // Fail all streams created after the last good stream ID. - for (Iterator> i = streams.entrySet().iterator(); - i.hasNext();) { - Map.Entry entry = i.next(); - int streamId = entry.getKey(); - if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { - entry.getValue().receiveRstStream(SpdyStream.RST_REFUSED_STREAM); - i.remove(); - } - } - } - } - - @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) { - SpdyStream stream = getStream(streamId); - if (stream != null) { - stream.receiveWindowUpdate(deltaWindowSize); - } + try { + handler.receive(synStream); + } catch (IOException e) { + throw new RuntimeException(e); + } } + }); } + + @Override public void synReply(int flags, int streamId, List nameValueBlock) + throws IOException { + SpdyStream replyStream = getStream(streamId); + if (replyStream == null) { + writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM); + return; + } + replyStream.receiveReply(nameValueBlock); + if ((flags & SpdyConnection.FLAG_FIN) != 0) { + replyStream.receiveFin(); + } + } + + @Override public void headers(int flags, int streamId, List nameValueBlock) + throws IOException { + SpdyStream replyStream = getStream(streamId); + if (replyStream != null) { + replyStream.receiveHeaders(nameValueBlock); + } + } + + @Override public void rstStream(int flags, int streamId, int statusCode) { + SpdyStream rstStream = removeStream(streamId); + if (rstStream != null) { + rstStream.receiveRstStream(statusCode); + } + } + + @Override public void settings(int flags, Settings newSettings) { + SpdyStream[] streamsToNotify = null; + synchronized (SpdyConnection.this) { + if (settings == null || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) { + settings = newSettings; + } else { + settings.merge(newSettings); + } + if (!streams.isEmpty()) { + streamsToNotify = streams.values().toArray(new SpdyStream[streams.size()]); + } + } + if (streamsToNotify != null) { + for (SpdyStream stream : streamsToNotify) { + // The synchronization here is ugly. We need to synchronize on 'this' to guard + // reads to 'settings'. We synchronize on 'stream' to guard the state change. + // And we need to acquire the 'stream' lock first, since that may block. + synchronized (stream) { + synchronized (this) { + stream.receiveSettings(settings); + } + } + } + } + } + + @Override public void noop() { + } + + @Override public void ping(int flags, int streamId) { + if (client != (streamId % 2 == 1)) { + // Respond to a client ping if this is a server and vice versa. + writePingLater(streamId, null); + } else { + Ping ping = removePing(streamId); + if (ping != null) { + ping.receive(); + } + } + } + + @Override public void goAway(int flags, int lastGoodStreamId, int statusCode) { + synchronized (SpdyConnection.this) { + shutdown = true; + + // Fail all streams created after the last good stream ID. + for (Iterator> i = streams.entrySet().iterator(); + i.hasNext(); ) { + Map.Entry entry = i.next(); + int streamId = entry.getKey(); + if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) { + entry.getValue().receiveRstStream(SpdyStream.RST_REFUSED_STREAM); + i.remove(); + } + } + } + } + + @Override public void windowUpdate(int flags, int streamId, int deltaWindowSize) { + SpdyStream stream = getStream(streamId); + if (stream != null) { + stream.receiveWindowUpdate(deltaWindowSize); + } + } + } } 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 d5abd3d70..db3b50c42 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java @@ -29,287 +29,286 @@ import java.util.zip.DataFormatException; import java.util.zip.Inflater; import java.util.zip.InflaterInputStream; -/** - * Read spdy/3 frames. - */ +/** Read spdy/3 frames. */ final class SpdyReader implements Closeable { - 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); + 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; - private int compressedLimit; + private final DataInputStream in; + private final DataInputStream nameValueBlockIn; + private int compressedLimit; - SpdyReader(InputStream in) { - this.in = new DataInputStream(in); - this.nameValueBlockIn = newNameValueBlockStream(); + 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(); - /** - * 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. + 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); } - int w2 = in.readInt(); + return result; + } + }; - boolean control = (w1 & 0x80000000) != 0; - int flags = (w2 & 0xff000000) >>> 24; - int length = (w2 & 0xffffff); + return new DataInputStream(new InflaterInputStream(throttleStream, inflater)); + } - if (control) { - int version = (w1 & 0x7fff0000) >>> 16; - int type = (w1 & 0xffff); + private List readNameValueBlock(int length) throws IOException { + this.compressedLimit += length; + try { + int numberOfPairs = nameValueBlockIn.readInt(); + 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 (version != 3) { - throw new ProtocolException("version != 3: " + version); - } + if (compressedLimit != 0) { + Logger.getLogger(getClass().getName()).warning("compressedLimit > 0: " + compressedLimit); + } - 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; - } + return entries; + } catch (DataFormatException e) { + throw new IOException(e); } + } - 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 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); } - - 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); + 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 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 static IOException ioException(String message, Object... args) throws IOException { + throw new IOException(String.format(message, args)); + } - 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); - } + @Override public void close() throws IOException { + Util.closeAll(in, nameValueBlockIn); + } - 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); - } + public interface Handler { + void data(int flags, int streamId, InputStream in, int length) throws IOException; - 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); - } + void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot, + List nameValueBlock); - @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(); - 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); - } - } - - 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); - } - - 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); - void settings(int flags, Settings settings); - void noop(); - void ping(int flags, int streamId); - void goAway(int flags, int lastGoodStreamId, int statusCode); - void windowUpdate(int flags, int streamId, int deltaWindowSize); - } + 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, 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 1006a54b6..7e80cf926 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java @@ -17,20 +17,19 @@ package com.squareup.okhttp.internal.spdy; import com.squareup.okhttp.internal.Util; -import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; -import static com.squareup.okhttp.internal.Util.pokeInt; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.SocketTimeoutException; -import static java.nio.ByteOrder.BIG_ENDIAN; import java.util.ArrayList; import java.util.List; -/** - * A logical bidirectional stream. - */ +import static com.squareup.okhttp.internal.Util.checkOffsetAndCount; +import static com.squareup.okhttp.internal.Util.pokeInt; +import static java.nio.ByteOrder.BIG_ENDIAN; + +/** A logical bidirectional stream. */ public final class SpdyStream { /* @@ -38,361 +37,344 @@ public final class SpdyStream { * blocking operations are performed while the lock is held. */ - private static final int DATA_FRAME_HEADER_LENGTH = 8; + private static final int DATA_FRAME_HEADER_LENGTH = 8; - private static final String[] STATUS_CODE_NAMES = { - null, - "PROTOCOL_ERROR", - "INVALID_STREAM", - "REFUSED_STREAM", - "UNSUPPORTED_VERSION", - "CANCEL", - "INTERNAL_ERROR", - "FLOW_CONTROL_ERROR", - "STREAM_IN_USE", - "STREAM_ALREADY_CLOSED", - "INVALID_CREDENTIALS", - "FRAME_TOO_LARGE", - }; + private static final String[] STATUS_CODE_NAMES = + { null, "PROTOCOL_ERROR", "INVALID_STREAM", "REFUSED_STREAM", "UNSUPPORTED_VERSION", "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; - public static final int RST_INVALID_STREAM = 2; - public static final int RST_REFUSED_STREAM = 3; - public static final int RST_UNSUPPORTED_VERSION = 4; - 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; + public static final int RST_PROTOCOL_ERROR = 1; + public static final int RST_INVALID_STREAM = 2; + public static final int RST_REFUSED_STREAM = 3; + public static final int RST_UNSUPPORTED_VERSION = 4; + 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; - /** - * The number of unacknowledged bytes at which the input stream will send - * the peer a {@code WINDOW_UPDATE} frame. Must be less than this client's - * window size, otherwise the remote peer will stop sending data on this - * stream. (Chrome 25 uses 5 MiB.) - */ - public static final int WINDOW_UPDATE_THRESHOLD = Settings.DEFAULT_INITIAL_WINDOW_SIZE / 2; + /** + * The number of unacknowledged bytes at which the input stream will send + * the peer a {@code WINDOW_UPDATE} frame. Must be less than this client's + * window size, otherwise the remote peer will stop sending data on this + * stream. (Chrome 25 uses 5 MiB.) + */ + public static final int WINDOW_UPDATE_THRESHOLD = Settings.DEFAULT_INITIAL_WINDOW_SIZE / 2; - private final int id; - private final SpdyConnection connection; - private final int priority; - private final int slot; - private long readTimeoutMillis = 0; - private int writeWindowSize; + private final int id; + private final SpdyConnection connection; + private final int priority; + private final int slot; + private long readTimeoutMillis = 0; + private int writeWindowSize; - /** Headers sent by the stream initiator. Immutable and non null. */ - private final List requestHeaders; + /** Headers sent by the stream initiator. Immutable and non null. */ + private final List requestHeaders; - /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */ - private List responseHeaders; + /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */ + private List responseHeaders; - private final SpdyDataInputStream in = new SpdyDataInputStream(); - private final SpdyDataOutputStream out = new SpdyDataOutputStream(); + private final SpdyDataInputStream in = new SpdyDataInputStream(); + private final SpdyDataOutputStream out = new SpdyDataOutputStream(); - /** - * The reason why this stream was abnormally closed. If there are multiple - * reasons to abnormally close this stream (such as both peers closing it - * near-simultaneously) then this is the first reason known to this peer. - */ - private int rstStatusCode = -1; + /** + * The reason why this stream was abnormally closed. If there are multiple + * reasons to abnormally close this stream (such as both peers closing it + * near-simultaneously) then this is the first reason known to this peer. + */ + private int rstStatusCode = -1; - SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot, - List requestHeaders, Settings settings) { - if (connection == null) throw new NullPointerException("connection == null"); - if (requestHeaders == null) throw new NullPointerException("requestHeaders == null"); - this.id = id; - this.connection = connection; - this.priority = priority; - this.slot = slot; - this.requestHeaders = requestHeaders; + SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot, + List requestHeaders, Settings settings) { + if (connection == null) throw new NullPointerException("connection == null"); + if (requestHeaders == null) throw new NullPointerException("requestHeaders == null"); + this.id = id; + this.connection = connection; + this.priority = priority; + this.slot = slot; + this.requestHeaders = requestHeaders; - if (isLocallyInitiated()) { - // I am the sender - in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0; - out.finished = (flags & SpdyConnection.FLAG_FIN) != 0; - } else { - // I am the receiver - in.finished = (flags & SpdyConnection.FLAG_FIN) != 0; - out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0; - } - - setSettings(settings); + if (isLocallyInitiated()) { + // I am the sender + in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0; + out.finished = (flags & SpdyConnection.FLAG_FIN) != 0; + } else { + // I am the receiver + in.finished = (flags & SpdyConnection.FLAG_FIN) != 0; + out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0; } - /** - * Returns true if this stream is open. A stream is open until either: - *
    - *
  • A {@code SYN_RESET} frame abnormally terminates the stream. - *
  • Both input and output streams have transmitted all data and - * headers. - *
- * Note that the input stream may continue to yield data even after a stream - * reports itself as not open. This is because input data is buffered. - */ - public synchronized boolean isOpen() { - if (rstStatusCode != -1) { - return false; - } - if ((in.finished || in.closed) && (out.finished || out.closed) && responseHeaders != null) { - return false; - } - return true; - } + setSettings(settings); + } - /** - * Returns true if this stream was created by this peer. - */ - public boolean isLocallyInitiated() { - boolean streamIsClient = (id % 2 == 1); - return connection.client == streamIsClient; + /** + * Returns true if this stream is open. A stream is open until either: + *
    + *
  • A {@code SYN_RESET} frame abnormally terminates the stream. + *
  • Both input and output streams have transmitted all data and + * headers. + *
+ * Note that the input stream may continue to yield data even after a stream + * reports itself as not open. This is because input data is buffered. + */ + public synchronized boolean isOpen() { + if (rstStatusCode != -1) { + return false; } - - public SpdyConnection getConnection() { - return connection; + if ((in.finished || in.closed) && (out.finished || out.closed) && responseHeaders != null) { + return false; } + return true; + } - public List getRequestHeaders() { - return requestHeaders; + /** Returns true if this stream was created by this peer. */ + public boolean isLocallyInitiated() { + boolean streamIsClient = (id % 2 == 1); + return connection.client == streamIsClient; + } + + public SpdyConnection getConnection() { + return connection; + } + + public List getRequestHeaders() { + return requestHeaders; + } + + /** + * Returns the stream's response headers, blocking if necessary if they + * have not been received yet. + */ + public synchronized List getResponseHeaders() throws IOException { + try { + while (responseHeaders == null && rstStatusCode == -1) { + wait(); + } + if (responseHeaders != null) { + return responseHeaders; + } + throw new IOException("stream was reset: " + rstStatusString()); + } catch (InterruptedException e) { + InterruptedIOException rethrow = new InterruptedIOException(); + rethrow.initCause(e); + throw rethrow; } + } - /** - * Returns the stream's response headers, blocking if necessary if they - * have not been received yet. - */ - public synchronized List getResponseHeaders() throws IOException { - try { - while (responseHeaders == null && rstStatusCode == -1) { - wait(); - } - if (responseHeaders != null) { - return responseHeaders; - } - throw new IOException("stream was reset: " + rstStatusString()); - } catch (InterruptedException e) { - InterruptedIOException rethrow = new InterruptedIOException(); - rethrow.initCause(e); - throw rethrow; - } + /** + * Returns the reason why this stream was closed, or -1 if it closed + * normally or has not yet been closed. Valid reasons are {@link + * #RST_PROTOCOL_ERROR}, {@link #RST_INVALID_STREAM}, {@link + * #RST_REFUSED_STREAM}, {@link #RST_UNSUPPORTED_VERSION}, {@link + * #RST_CANCEL}, {@link #RST_INTERNAL_ERROR} and {@link + * #RST_FLOW_CONTROL_ERROR}. + */ + public synchronized int getRstStatusCode() { + return rstStatusCode; + } + + /** + * Sends a reply to an incoming stream. + * + * @param out true to create an output stream that we can use to send data + * to the remote peer. Corresponds to {@code FLAG_FIN}. + */ + public void reply(List responseHeaders, boolean out) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + int flags = 0; + synchronized (this) { + if (responseHeaders == null) { + throw new NullPointerException("responseHeaders == null"); + } + if (isLocallyInitiated()) { + throw new IllegalStateException("cannot reply to a locally initiated stream"); + } + if (this.responseHeaders != null) { + throw new IllegalStateException("reply already sent"); + } + this.responseHeaders = responseHeaders; + if (!out) { + this.out.finished = true; + flags |= SpdyConnection.FLAG_FIN; + } } + connection.writeSynReply(id, flags, responseHeaders); + } - /** - * Returns the reason why this stream was closed, or -1 if it closed - * normally or has not yet been closed. Valid reasons are {@link - * #RST_PROTOCOL_ERROR}, {@link #RST_INVALID_STREAM}, {@link - * #RST_REFUSED_STREAM}, {@link #RST_UNSUPPORTED_VERSION}, {@link - * #RST_CANCEL}, {@link #RST_INTERNAL_ERROR} and {@link - * #RST_FLOW_CONTROL_ERROR}. - */ - public synchronized int getRstStatusCode() { - return rstStatusCode; + /** + * Sets the maximum time to wait on input stream reads before failing with a + * {@code SocketTimeoutException}, or {@code 0} to wait indefinitely. + */ + public void setReadTimeout(long readTimeoutMillis) { + this.readTimeoutMillis = readTimeoutMillis; + } + + public long getReadTimeoutMillis() { + return readTimeoutMillis; + } + + /** Returns an input stream that can be used to read data from the peer. */ + public InputStream getInputStream() { + return in; + } + + /** + * Returns an output stream that can be used to write data to the peer. + * + * @throws IllegalStateException if this stream was initiated by the peer + * and a {@link #reply} has not yet been sent. + */ + public OutputStream getOutputStream() { + synchronized (this) { + if (responseHeaders == null && !isLocallyInitiated()) { + throw new IllegalStateException("reply before requesting the output stream"); + } } + return out; + } - /** - * Sends a reply to an incoming stream. - * - * @param out true to create an output stream that we can use to send data - * to the remote peer. Corresponds to {@code FLAG_FIN}. - */ - public void reply(List responseHeaders, boolean out) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - int flags = 0; - synchronized (this) { - if (responseHeaders == null) { - throw new NullPointerException("responseHeaders == null"); - } - if (isLocallyInitiated()) { - throw new IllegalStateException("cannot reply to a locally initiated stream"); - } - if (this.responseHeaders != null) { - throw new IllegalStateException("reply already sent"); - } - this.responseHeaders = responseHeaders; - if (!out) { - this.out.finished = true; - flags |= SpdyConnection.FLAG_FIN; - } - } - connection.writeSynReply(id, flags, responseHeaders); + /** + * Abnormally terminate this stream. This blocks until the {@code RST_STREAM} + * frame has been transmitted. + */ + public void close(int rstStatusCode) throws IOException { + if (!closeInternal(rstStatusCode)) { + return; // Already closed. } + connection.writeSynReset(id, rstStatusCode); + } - /** - * Sets the maximum time to wait on input stream reads before failing with a - * {@code SocketTimeoutException}, or {@code 0} to wait indefinitely. - */ - public void setReadTimeout(long readTimeoutMillis) { - this.readTimeoutMillis = readTimeoutMillis; + /** + * Abnormally terminate this stream. This enqueues a {@code RST_STREAM} + * frame and returns immediately. + */ + public void closeLater(int rstStatusCode) { + if (!closeInternal(rstStatusCode)) { + return; // Already closed. } + connection.writeSynResetLater(id, rstStatusCode); + } - public long getReadTimeoutMillis() { - return readTimeoutMillis; + /** Returns true if this stream was closed. */ + private boolean closeInternal(int rstStatusCode) { + assert (!Thread.holdsLock(this)); + synchronized (this) { + if (this.rstStatusCode != -1) { + return false; + } + if (in.finished && out.finished) { + return false; + } + this.rstStatusCode = rstStatusCode; + notifyAll(); } + connection.removeStream(id); + return true; + } - /** - * Returns an input stream that can be used to read data from the peer. - */ - public InputStream getInputStream() { - return in; - } - - /** - * Returns an output stream that can be used to write data to the peer. - * - * @throws IllegalStateException if this stream was initiated by the peer - * and a {@link #reply} has not yet been sent. - */ - public OutputStream getOutputStream() { - synchronized (this) { - if (responseHeaders == null && !isLocallyInitiated()) { - throw new IllegalStateException("reply before requesting the output stream"); - } - } - return out; - } - - /** - * Abnormally terminate this stream. This blocks until the {@code RST_STREAM} - * frame has been transmitted. - */ - public void close(int rstStatusCode) throws IOException { - if (!closeInternal(rstStatusCode)) { - return; // Already closed. - } - connection.writeSynReset(id, rstStatusCode); - } - - /** - * Abnormally terminate this stream. This enqueues a {@code RST_STREAM} - * frame and returns immediately. - */ - public void closeLater(int rstStatusCode) { - if (!closeInternal(rstStatusCode)) { - return; // Already closed. - } - connection.writeSynResetLater(id, rstStatusCode); - } - - /** - * Returns true if this stream was closed. - */ - private boolean closeInternal(int rstStatusCode) { - assert (!Thread.holdsLock(this)); - synchronized (this) { - if (this.rstStatusCode != -1) { - return false; - } - if (in.finished && out.finished) { - return false; - } - this.rstStatusCode = rstStatusCode; - notifyAll(); - } - connection.removeStream(id); - return true; - } - - void receiveReply(List strings) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean streamInUseError = false; - boolean open = true; - synchronized (this) { - if (isLocallyInitiated() && responseHeaders == null) { - responseHeaders = strings; - open = isOpen(); - notifyAll(); - } else { - streamInUseError = true; - } - } - if (streamInUseError) { - closeLater(SpdyStream.RST_STREAM_IN_USE); - } else if (!open) { - connection.removeStream(id); - } - } - - void receiveHeaders(List headers) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean protocolError = false; - synchronized (this) { - if (responseHeaders != null) { - List newHeaders = new ArrayList(); - newHeaders.addAll(responseHeaders); - newHeaders.addAll(headers); - this.responseHeaders = newHeaders; - } else { - protocolError = true; - } - } - if (protocolError) { - closeLater(SpdyStream.RST_PROTOCOL_ERROR); - } - } - - void receiveData(InputStream in, int length) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - this.in.receive(in, length); - } - - void receiveFin() { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean open; - synchronized (this) { - this.in.finished = true; - open = isOpen(); - notifyAll(); - } - if (!open) { - connection.removeStream(id); - } - } - - synchronized void receiveRstStream(int statusCode) { - if (rstStatusCode == -1) { - rstStatusCode = statusCode; - notifyAll(); - } - } - - private void setSettings(Settings settings) { - assert (Thread.holdsLock(connection)); // Because 'settings' is guarded by 'connection'. - this.writeWindowSize = settings != null - ? settings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE) - : Settings.DEFAULT_INITIAL_WINDOW_SIZE; - } - - void receiveSettings(Settings settings) { - assert (Thread.holdsLock(this)); - setSettings(settings); + void receiveReply(List strings) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean streamInUseError = false; + boolean open = true; + synchronized (this) { + if (isLocallyInitiated() && responseHeaders == null) { + responseHeaders = strings; + open = isOpen(); notifyAll(); + } else { + streamInUseError = true; + } } - - synchronized void receiveWindowUpdate(int deltaWindowSize) { - out.unacknowledgedBytes -= deltaWindowSize; - notifyAll(); + if (streamInUseError) { + closeLater(SpdyStream.RST_STREAM_IN_USE); + } else if (!open) { + connection.removeStream(id); } + } - private String rstStatusString() { - return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length - ? STATUS_CODE_NAMES[rstStatusCode] - : Integer.toString(rstStatusCode); + void receiveHeaders(List headers) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean protocolError = false; + synchronized (this) { + if (responseHeaders != null) { + List newHeaders = new ArrayList(); + newHeaders.addAll(responseHeaders); + newHeaders.addAll(headers); + this.responseHeaders = newHeaders; + } else { + protocolError = true; + } } - - int getPriority() { - return priority; + if (protocolError) { + closeLater(SpdyStream.RST_PROTOCOL_ERROR); } + } - int getSlot() { - return slot; + void receiveData(InputStream in, int length) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + this.in.receive(in, length); + } + + void receiveFin() { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean open; + synchronized (this) { + this.in.finished = true; + open = isOpen(); + notifyAll(); } + if (!open) { + connection.removeStream(id); + } + } - /** - * An input stream that reads the incoming data frames of a stream. Although - * this class uses synchronization to safely receive incoming data frames, - * it is not intended for use by multiple readers. - */ - private final class SpdyDataInputStream extends InputStream { + synchronized void receiveRstStream(int statusCode) { + if (rstStatusCode == -1) { + rstStatusCode = statusCode; + notifyAll(); + } + } + + private void setSettings(Settings settings) { + assert (Thread.holdsLock(connection)); // Because 'settings' is guarded by 'connection'. + this.writeWindowSize = + settings != null ? settings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE) + : Settings.DEFAULT_INITIAL_WINDOW_SIZE; + } + + void receiveSettings(Settings settings) { + assert (Thread.holdsLock(this)); + setSettings(settings); + notifyAll(); + } + + synchronized void receiveWindowUpdate(int deltaWindowSize) { + out.unacknowledgedBytes -= deltaWindowSize; + notifyAll(); + } + + private String rstStatusString() { + return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length + ? STATUS_CODE_NAMES[rstStatusCode] : 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, + * it is not intended for use by multiple readers. + */ + private final class SpdyDataInputStream extends InputStream { /* * Store incoming data bytes in a circular buffer. When the buffer is * empty, pos == -1. Otherwise pos is the first byte to read and limit @@ -407,339 +389,339 @@ public final class SpdyStream { * limit pos */ - private final byte[] buffer = new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE]; + private final byte[] buffer = new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE]; - /** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */ - private int pos = -1; + /** the next byte to be read, or -1 if the buffer is empty. Never buffer.length */ + private int pos = -1; - /** the last byte to be read. Never buffer.length */ - private int limit; + /** the last byte to be read. Never buffer.length */ + private int limit; - /** True if the caller has closed this stream. */ - private boolean closed; + /** True if the caller has closed this stream. */ + private boolean closed; - /** - * True if either side has cleanly shut down this stream. We will - * receive no more bytes beyond those already in the buffer. - */ - private boolean finished; + /** + * True if either side has cleanly shut down this stream. We will + * receive no more bytes beyond those already in the buffer. + */ + private boolean finished; - /** - * The total number of bytes consumed by the application (with {@link - * #read}), but not yet acknowledged by sending a {@code WINDOW_UPDATE} - * frame. - */ - private int unacknowledgedBytes = 0; + /** + * The total number of bytes consumed by the application (with {@link + * #read}), but not yet acknowledged by sending a {@code WINDOW_UPDATE} + * frame. + */ + private int unacknowledgedBytes = 0; - @Override public int available() throws IOException { - synchronized (SpdyStream.this) { - checkNotClosed(); - if (pos == -1) { - return 0; - } else if (limit > pos) { - return limit - pos; - } else { - return limit + (buffer.length - pos); - } - } - } - - @Override public int read() throws IOException { - return Util.readSingleByte(this); - } - - @Override public int read(byte[] b, int offset, int count) throws IOException { - synchronized (SpdyStream.this) { - checkOffsetAndCount(b.length, offset, count); - waitUntilReadable(); - checkNotClosed(); - - if (pos == -1) { - return -1; - } - - int copied = 0; - - // drain from [pos..buffer.length) - if (limit <= pos) { - int bytesToCopy = Math.min(count, buffer.length - pos); - System.arraycopy(buffer, pos, b, offset, bytesToCopy); - pos += bytesToCopy; - copied += bytesToCopy; - if (pos == buffer.length) { - pos = 0; - } - } - - // drain from [pos..limit) - if (copied < count) { - int bytesToCopy = Math.min(limit - pos, count - copied); - System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy); - pos += bytesToCopy; - copied += bytesToCopy; - } - - // Flow control: notify the peer that we're ready for more data! - unacknowledgedBytes += copied; - if (unacknowledgedBytes >= WINDOW_UPDATE_THRESHOLD) { - connection.writeWindowUpdateLater(id, unacknowledgedBytes); - unacknowledgedBytes = 0; - } - - if (pos == limit) { - pos = -1; - limit = 0; - } - - return copied; - } - } - - /** - * Returns once the input stream is either readable or finished. Throws - * a {@link SocketTimeoutException} if the read timeout elapses before - * that happens. - */ - private void waitUntilReadable() throws IOException { - long start = 0; - long remaining = 0; - if (readTimeoutMillis != 0) { - start = (System.nanoTime() / 1000000); - remaining = readTimeoutMillis; - } - try { - while (pos == -1 && !finished && !closed && rstStatusCode == -1) { - if (readTimeoutMillis == 0) { - SpdyStream.this.wait(); - } else if (remaining > 0) { - SpdyStream.this.wait(remaining); - remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000); - } else { - throw new SocketTimeoutException(); - } - } - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } - } - - void receive(InputStream in, int byteCount) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - - if (byteCount == 0) { - return; - } - - int pos; - int limit; - int firstNewByte; - boolean finished; - boolean flowControlError; - synchronized (SpdyStream.this) { - finished = this.finished; - pos = this.pos; - firstNewByte = this.limit; - limit = this.limit; - flowControlError = byteCount > buffer.length - available(); - } - - // If the peer sends more data than we can handle, discard it and close the connection. - if (flowControlError) { - Util.skipByReading(in, byteCount); - closeLater(SpdyStream.RST_FLOW_CONTROL_ERROR); - return; - } - - // Discard data received after the stream is finished. It's probably a benign race. - if (finished) { - Util.skipByReading(in, byteCount); - return; - } - - // Fill the buffer without holding any locks. First fill [limit..buffer.length) if that - // won't overwrite unread data. Then fill [limit..pos). We can't hold a lock, otherwise - // writes will be blocked until reads complete. - if (pos < limit) { - int firstCopyCount = Math.min(byteCount, buffer.length - limit); - Util.readFully(in, buffer, limit, firstCopyCount); - limit += firstCopyCount; - byteCount -= firstCopyCount; - if (limit == buffer.length) { - limit = 0; - } - } - if (byteCount > 0) { - Util.readFully(in, buffer, limit, byteCount); - limit += byteCount; - } - - synchronized (SpdyStream.this) { - // Update the new limit, and mark the position as readable if necessary. - this.limit = limit; - if (this.pos == -1) { - this.pos = firstNewByte; - SpdyStream.this.notifyAll(); - } - } - } - - @Override public void close() throws IOException { - synchronized (SpdyStream.this) { - closed = true; - SpdyStream.this.notifyAll(); - } - cancelStreamIfNecessary(); - } - - private void checkNotClosed() throws IOException { - if (closed) { - throw new IOException("stream closed"); - } - if (rstStatusCode != -1) { - throw new IOException("stream was reset: " + rstStatusString()); - } + @Override public int available() throws IOException { + synchronized (SpdyStream.this) { + checkNotClosed(); + if (pos == -1) { + return 0; + } else if (limit > pos) { + return limit - pos; + } else { + return limit + (buffer.length - pos); } + } } - private void cancelStreamIfNecessary() throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - boolean open; - boolean cancel; - synchronized (this) { - cancel = !in.finished && in.closed && (out.finished || out.closed); - open = isOpen(); + @Override public int read() throws IOException { + return Util.readSingleByte(this); + } + + @Override public int read(byte[] b, int offset, int count) throws IOException { + synchronized (SpdyStream.this) { + checkOffsetAndCount(b.length, offset, count); + waitUntilReadable(); + checkNotClosed(); + + if (pos == -1) { + return -1; } - if (cancel) { - // RST this stream to prevent additional data from being sent. This - // is safe because the input stream is closed (we won't use any - // further bytes) and the output stream is either finished or closed - // (so RSTing both streams doesn't cause harm). - SpdyStream.this.close(RST_CANCEL); - } else if (!open) { - connection.removeStream(id); + + int copied = 0; + + // drain from [pos..buffer.length) + if (limit <= pos) { + int bytesToCopy = Math.min(count, buffer.length - pos); + System.arraycopy(buffer, pos, b, offset, bytesToCopy); + pos += bytesToCopy; + copied += bytesToCopy; + if (pos == buffer.length) { + pos = 0; + } } + + // drain from [pos..limit) + if (copied < count) { + int bytesToCopy = Math.min(limit - pos, count - copied); + System.arraycopy(buffer, pos, b, offset + copied, bytesToCopy); + pos += bytesToCopy; + copied += bytesToCopy; + } + + // Flow control: notify the peer that we're ready for more data! + unacknowledgedBytes += copied; + if (unacknowledgedBytes >= WINDOW_UPDATE_THRESHOLD) { + connection.writeWindowUpdateLater(id, unacknowledgedBytes); + unacknowledgedBytes = 0; + } + + if (pos == limit) { + pos = -1; + limit = 0; + } + + return copied; + } } /** - * An output stream that writes outgoing data frames of a stream. This class - * is not thread safe. + * Returns once the input stream is either readable or finished. Throws + * a {@link SocketTimeoutException} if the read timeout elapses before + * that happens. */ - private final class SpdyDataOutputStream extends OutputStream { - private final byte[] buffer = new byte[8192]; - private int pos = DATA_FRAME_HEADER_LENGTH; - - /** True if the caller has closed this stream. */ - private boolean closed; - - /** - * True if either side has cleanly shut down this stream. We shall send - * no more bytes. - */ - private boolean finished; - - /** - * The total number of bytes written out to the peer, but not yet - * acknowledged with an incoming {@code WINDOW_UPDATE} frame. Writes - * block if they cause this to exceed the {@code WINDOW_SIZE}. - */ - private int unacknowledgedBytes = 0; - - @Override public void write(int b) throws IOException { - Util.writeSingleByte(this, b); - } - - @Override public void write(byte[] bytes, int offset, int count) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - checkOffsetAndCount(bytes.length, offset, count); - checkNotClosed(); - - while (count > 0) { - if (pos == buffer.length) { - writeFrame(false); - } - int bytesToCopy = Math.min(count, buffer.length - pos); - System.arraycopy(bytes, offset, buffer, pos, bytesToCopy); - pos += bytesToCopy; - offset += bytesToCopy; - count -= bytesToCopy; - } - } - - @Override public void flush() throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - checkNotClosed(); - if (pos > DATA_FRAME_HEADER_LENGTH) { - writeFrame(false); - connection.flush(); - } - } - - @Override public void close() throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - synchronized (SpdyStream.this) { - if (closed) { - return; - } - closed = true; - } - writeFrame(true); - connection.flush(); - cancelStreamIfNecessary(); - } - - private void writeFrame(boolean last) throws IOException { - assert (!Thread.holdsLock(SpdyStream.this)); - - int length = pos - DATA_FRAME_HEADER_LENGTH; - synchronized (SpdyStream.this) { - waitUntilWritable(length, last); - unacknowledgedBytes += length; - } - int flags = 0; - if (last) { - flags |= SpdyConnection.FLAG_FIN; - } - pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN); - pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN); - connection.writeFrame(buffer, 0, pos); - pos = DATA_FRAME_HEADER_LENGTH; - } - - /** - * Returns once the peer is ready to receive {@code count} bytes. - * - * @throws IOException if the stream was finished or closed, or the - * thread was interrupted. - */ - private void waitUntilWritable(int count, boolean last) throws IOException { - try { - while (unacknowledgedBytes + count >= writeWindowSize) { - SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE. - - // The stream may have been closed or reset while we were waiting! - if (!last && closed) { - throw new IOException("stream closed"); - } else if (finished) { - throw new IOException("stream finished"); - } else if (rstStatusCode != -1) { - throw new IOException("stream was reset: " + rstStatusString()); - } - } - } catch (InterruptedException e) { - throw new InterruptedIOException(); - } - } - - private void checkNotClosed() throws IOException { - synchronized (SpdyStream.this) { - if (closed) { - throw new IOException("stream closed"); - } else if (finished) { - throw new IOException("stream finished"); - } else if (rstStatusCode != -1) { - throw new IOException("stream was reset: " + rstStatusString()); - } - } + private void waitUntilReadable() throws IOException { + long start = 0; + long remaining = 0; + if (readTimeoutMillis != 0) { + start = (System.nanoTime() / 1000000); + remaining = readTimeoutMillis; + } + try { + while (pos == -1 && !finished && !closed && rstStatusCode == -1) { + if (readTimeoutMillis == 0) { + SpdyStream.this.wait(); + } else if (remaining > 0) { + SpdyStream.this.wait(remaining); + remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000); + } else { + throw new SocketTimeoutException(); + } } + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } } + + void receive(InputStream in, int byteCount) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + + if (byteCount == 0) { + return; + } + + int pos; + int limit; + int firstNewByte; + boolean finished; + boolean flowControlError; + synchronized (SpdyStream.this) { + finished = this.finished; + pos = this.pos; + firstNewByte = this.limit; + limit = this.limit; + flowControlError = byteCount > buffer.length - available(); + } + + // If the peer sends more data than we can handle, discard it and close the connection. + if (flowControlError) { + Util.skipByReading(in, byteCount); + closeLater(SpdyStream.RST_FLOW_CONTROL_ERROR); + return; + } + + // Discard data received after the stream is finished. It's probably a benign race. + if (finished) { + Util.skipByReading(in, byteCount); + return; + } + + // Fill the buffer without holding any locks. First fill [limit..buffer.length) if that + // won't overwrite unread data. Then fill [limit..pos). We can't hold a lock, otherwise + // writes will be blocked until reads complete. + if (pos < limit) { + int firstCopyCount = Math.min(byteCount, buffer.length - limit); + Util.readFully(in, buffer, limit, firstCopyCount); + limit += firstCopyCount; + byteCount -= firstCopyCount; + if (limit == buffer.length) { + limit = 0; + } + } + if (byteCount > 0) { + Util.readFully(in, buffer, limit, byteCount); + limit += byteCount; + } + + synchronized (SpdyStream.this) { + // Update the new limit, and mark the position as readable if necessary. + this.limit = limit; + if (this.pos == -1) { + this.pos = firstNewByte; + SpdyStream.this.notifyAll(); + } + } + } + + @Override public void close() throws IOException { + synchronized (SpdyStream.this) { + closed = true; + SpdyStream.this.notifyAll(); + } + cancelStreamIfNecessary(); + } + + private void checkNotClosed() throws IOException { + if (closed) { + throw new IOException("stream closed"); + } + if (rstStatusCode != -1) { + throw new IOException("stream was reset: " + rstStatusString()); + } + } + } + + private void cancelStreamIfNecessary() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + boolean open; + boolean cancel; + synchronized (this) { + cancel = !in.finished && in.closed && (out.finished || out.closed); + open = isOpen(); + } + if (cancel) { + // RST this stream to prevent additional data from being sent. This + // is safe because the input stream is closed (we won't use any + // further bytes) and the output stream is either finished or closed + // (so RSTing both streams doesn't cause harm). + SpdyStream.this.close(RST_CANCEL); + } else if (!open) { + connection.removeStream(id); + } + } + + /** + * An output stream that writes outgoing data frames of a stream. This class + * is not thread safe. + */ + private final class SpdyDataOutputStream extends OutputStream { + private final byte[] buffer = new byte[8192]; + private int pos = DATA_FRAME_HEADER_LENGTH; + + /** True if the caller has closed this stream. */ + private boolean closed; + + /** + * True if either side has cleanly shut down this stream. We shall send + * no more bytes. + */ + private boolean finished; + + /** + * The total number of bytes written out to the peer, but not yet + * acknowledged with an incoming {@code WINDOW_UPDATE} frame. Writes + * block if they cause this to exceed the {@code WINDOW_SIZE}. + */ + private int unacknowledgedBytes = 0; + + @Override public void write(int b) throws IOException { + Util.writeSingleByte(this, b); + } + + @Override public void write(byte[] bytes, int offset, int count) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + checkOffsetAndCount(bytes.length, offset, count); + checkNotClosed(); + + while (count > 0) { + if (pos == buffer.length) { + writeFrame(false); + } + int bytesToCopy = Math.min(count, buffer.length - pos); + System.arraycopy(bytes, offset, buffer, pos, bytesToCopy); + pos += bytesToCopy; + offset += bytesToCopy; + count -= bytesToCopy; + } + } + + @Override public void flush() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + checkNotClosed(); + if (pos > DATA_FRAME_HEADER_LENGTH) { + writeFrame(false); + connection.flush(); + } + } + + @Override public void close() throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + synchronized (SpdyStream.this) { + if (closed) { + return; + } + closed = true; + } + writeFrame(true); + connection.flush(); + cancelStreamIfNecessary(); + } + + private void writeFrame(boolean last) throws IOException { + assert (!Thread.holdsLock(SpdyStream.this)); + + int length = pos - DATA_FRAME_HEADER_LENGTH; + synchronized (SpdyStream.this) { + waitUntilWritable(length, last); + unacknowledgedBytes += length; + } + int flags = 0; + if (last) { + flags |= SpdyConnection.FLAG_FIN; + } + pokeInt(buffer, 0, id & 0x7fffffff, BIG_ENDIAN); + pokeInt(buffer, 4, (flags & 0xff) << 24 | length & 0xffffff, BIG_ENDIAN); + connection.writeFrame(buffer, 0, pos); + pos = DATA_FRAME_HEADER_LENGTH; + } + + /** + * Returns once the peer is ready to receive {@code count} bytes. + * + * @throws IOException if the stream was finished or closed, or the + * thread was interrupted. + */ + private void waitUntilWritable(int count, boolean last) throws IOException { + try { + while (unacknowledgedBytes + count >= writeWindowSize) { + SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE. + + // The stream may have been closed or reset while we were waiting! + if (!last && closed) { + throw new IOException("stream closed"); + } else if (finished) { + throw new IOException("stream finished"); + } else if (rstStatusCode != -1) { + throw new IOException("stream was reset: " + rstStatusString()); + } + } + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } + } + + private void checkNotClosed() throws IOException { + synchronized (SpdyStream.this) { + if (closed) { + throw new IOException("stream closed"); + } else if (finished) { + throw new IOException("stream finished"); + } else if (rstStatusCode != -1) { + throw new IOException("stream was reset: " + rstStatusString()); + } + } + } + } } 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 b884acbde..b3d1d1f9a 100644 --- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java +++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java @@ -26,153 +26,151 @@ import java.io.OutputStream; import java.util.List; import java.util.zip.Deflater; -/** - * Write spdy/3 frames. - */ +/** Write spdy/3 frames. */ final class SpdyWriter implements Closeable { - final DataOutputStream out; - private final ByteArrayOutputStream nameValueBlockBuffer; - private final DataOutputStream nameValueBlockOut; + final DataOutputStream out; + private final ByteArrayOutputStream nameValueBlockBuffer; + private final DataOutputStream nameValueBlockOut; - SpdyWriter(OutputStream out) { - this.out = new DataOutputStream(out); + 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)); + 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 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 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 synReply( - int flags, int streamId, List nameValueBlock) throws IOException { - writeNameValueBlockToBuffer(nameValueBlock); - int type = SpdyConnection.TYPE_SYN_REPLY; - int length = nameValueBlockBuffer.size() + 4; + 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(); + } - 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 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 headers( - int flags, int streamId, List nameValueBlock) throws IOException { - writeNameValueBlockToBuffer(nameValueBlock); - int type = SpdyConnection.TYPE_HEADERS; - int length = nameValueBlockBuffer.size() + 4; + 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(); + } - 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 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(); + } - 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); - } + @Override public void close() throws IOException { + Util.closeAll(out, nameValueBlockOut); + } } diff --git a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java index 6a5997fb3..dca9625bd 100644 --- a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java +++ b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java @@ -41,6 +41,7 @@ import static org.junit.Assert.assertTrue; public final class ConnectionPoolTest { private static final int KEEP_ALIVE_DURATION_MS = 500; private static final SSLContext sslContext; + static { try { sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); @@ -50,6 +51,7 @@ public final class ConnectionPoolTest { throw new RuntimeException(e); } } + private final MockSpdyServer spdyServer = new MockSpdyServer(sslContext.getSocketFactory()); private InetSocketAddress spdySocketAddress; private Address spdyAddress; @@ -73,8 +75,9 @@ public final class ConnectionPoolTest { httpServer.getPort()); spdyServer.play(); - spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), - sslContext.getSocketFactory(), new RecordingHostnameVerifier(), null); + spdyAddress = + new Address(spdyServer.getHostName(), spdyServer.getPort(), sslContext.getSocketFactory(), + new RecordingHostnameVerifier(), null); spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()), spdyServer.getPort()); @@ -229,10 +232,10 @@ public final class ConnectionPoolTest { @Test public void validateIdleSpdyConnectionTimeout() throws Exception { ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS); pool.maybeShare(spdyA); - Thread.sleep((int)(KEEP_ALIVE_DURATION_MS * 0.7)); + Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7)); assertNull(pool.get(httpAddress)); assertPooled(pool, spdyA); // Connection should still be in the pool. - Thread.sleep((int)(KEEP_ALIVE_DURATION_MS * 0.4)); + Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4)); assertNull(pool.get(httpAddress)); assertPooled(pool); } @@ -240,10 +243,10 @@ public final class ConnectionPoolTest { @Test public void validateIdleHttpConnectionTimeout() throws Exception { ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS); pool.recycle(httpA); - Thread.sleep((int)(KEEP_ALIVE_DURATION_MS * 0.7)); + Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7)); assertNull(pool.get(spdyAddress)); assertPooled(pool, httpA); // Connection should still be in the pool. - Thread.sleep((int)(KEEP_ALIVE_DURATION_MS * 0.4)); + Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4)); assertNull(pool.get(spdyAddress)); assertPooled(pool); } @@ -385,5 +388,4 @@ public final class ConnectionPoolTest { private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception { assertEquals(Arrays.asList(connections), pool.getConnections()); } - } diff --git a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java b/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java index 160ec7856..72cc70d52 100644 --- a/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java +++ b/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java @@ -16,10 +16,6 @@ package com.squareup.okhttp.internal; -import com.squareup.okhttp.internal.DiskLruCache; -import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE; -import static com.squareup.okhttp.internal.DiskLruCache.MAGIC; -import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -32,771 +28,775 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE; +import static com.squareup.okhttp.internal.DiskLruCache.MAGIC; +import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.junit.Before; -import org.junit.Test; public final class DiskLruCacheTest { - private final int appVersion = 100; - private String javaTmpDir; - private File cacheDir; - private File journalFile; - private DiskLruCache cache; + private final int appVersion = 100; + private String javaTmpDir; + private File cacheDir; + private File journalFile; + private DiskLruCache cache; - @Before public void setUp() throws Exception { - javaTmpDir = System.getProperty("java.io.tmpdir"); - cacheDir = new File(javaTmpDir, "DiskLruCacheTest"); - cacheDir.mkdir(); - journalFile = new File(cacheDir, JOURNAL_FILE); - for (File file : cacheDir.listFiles()) { - file.delete(); - } - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + @Before public void setUp() throws Exception { + javaTmpDir = System.getProperty("java.io.tmpdir"); + cacheDir = new File(javaTmpDir, "DiskLruCacheTest"); + cacheDir.mkdir(); + journalFile = new File(cacheDir, JOURNAL_FILE); + for (File file : cacheDir.listFiles()) { + file.delete(); + } + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + } + + @After public void tearDown() throws Exception { + cache.close(); + } + + @Test public void emptyCache() throws Exception { + cache.close(); + assertJournalEquals(); + } + + @Test public void writeAndReadEntry() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + creator.set(0, "ABC"); + creator.set(1, "DE"); + assertNull(creator.getString(0)); + assertNull(creator.newInputStream(0)); + assertNull(creator.getString(1)); + assertNull(creator.newInputStream(1)); + creator.commit(); + + DiskLruCache.Snapshot snapshot = cache.get("k1"); + assertEquals("ABC", snapshot.getString(0)); + assertEquals("DE", snapshot.getString(1)); + } + + @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + creator.set(0, "A"); + creator.set(1, "B"); + creator.commit(); + cache.close(); + + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + DiskLruCache.Snapshot snapshot = cache.get("k1"); + assertEquals("A", snapshot.getString(0)); + assertEquals("B", snapshot.getString(1)); + snapshot.close(); + } + + @Test public void journalWithEditAndPublish() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed + creator.set(0, "AB"); + creator.set(1, "C"); + creator.commit(); + cache.close(); + assertJournalEquals("DIRTY k1", "CLEAN k1 2 1"); + } + + @Test public void revertedNewFileIsRemoveInJournal() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed + creator.set(0, "AB"); + creator.set(1, "C"); + creator.abort(); + cache.close(); + assertJournalEquals("DIRTY k1", "REMOVE k1"); + } + + @Test public void unterminatedEditIsRevertedOnClose() throws Exception { + cache.edit("k1"); + cache.close(); + assertJournalEquals("DIRTY k1", "REMOVE k1"); + } + + @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + assertNull(cache.get("k1")); + creator.set(0, "A"); + creator.set(1, "BC"); + creator.commit(); + cache.close(); + assertJournalEquals("DIRTY k1", "CLEAN k1 1 2"); + } + + @Test public void journalWithEditAndPublishAndRead() throws Exception { + DiskLruCache.Editor k1Creator = cache.edit("k1"); + k1Creator.set(0, "AB"); + k1Creator.set(1, "C"); + k1Creator.commit(); + DiskLruCache.Editor k2Creator = cache.edit("k2"); + k2Creator.set(0, "DEF"); + k2Creator.set(1, "G"); + k2Creator.commit(); + DiskLruCache.Snapshot k1Snapshot = cache.get("k1"); + k1Snapshot.close(); + cache.close(); + assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1"); + } + + @Test public void cannotOperateOnEditAfterPublish() throws Exception { + DiskLruCache.Editor editor = cache.edit("k1"); + editor.set(0, "A"); + editor.set(1, "B"); + editor.commit(); + assertInoperable(editor); + } + + @Test public void cannotOperateOnEditAfterRevert() throws Exception { + DiskLruCache.Editor editor = cache.edit("k1"); + editor.set(0, "A"); + editor.set(1, "B"); + editor.abort(); + assertInoperable(editor); + } + + @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception { + DiskLruCache.Editor editor = cache.edit("k1"); + editor.set(0, "ABC"); + editor.set(1, "B"); + editor.commit(); + File k1 = getCleanFile("k1", 0); + assertEquals("ABC", readFile(k1)); + cache.remove("k1"); + assertFalse(k1.exists()); + } + + /** + * Each read sees a snapshot of the file at the time read was called. + * This means that two reads of the same key can see different data. + */ + @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception { + DiskLruCache.Editor v1Creator = cache.edit("k1"); + v1Creator.set(0, "AAaa"); + v1Creator.set(1, "BBbb"); + v1Creator.commit(); + + DiskLruCache.Snapshot snapshot1 = cache.get("k1"); + InputStream inV1 = snapshot1.getInputStream(0); + assertEquals('A', inV1.read()); + assertEquals('A', inV1.read()); + + DiskLruCache.Editor v1Updater = cache.edit("k1"); + v1Updater.set(0, "CCcc"); + v1Updater.set(1, "DDdd"); + v1Updater.commit(); + + DiskLruCache.Snapshot snapshot2 = cache.get("k1"); + assertEquals("CCcc", snapshot2.getString(0)); + assertEquals("DDdd", snapshot2.getString(1)); + snapshot2.close(); + + assertEquals('a', inV1.read()); + assertEquals('a', inV1.read()); + assertEquals("BBbb", snapshot1.getString(1)); + snapshot1.close(); + } + + @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception { + cache.close(); + File cleanFile0 = getCleanFile("k1", 0); + File cleanFile1 = getCleanFile("k1", 1); + File dirtyFile0 = getDirtyFile("k1", 0); + File dirtyFile1 = getDirtyFile("k1", 1); + writeFile(cleanFile0, "A"); + writeFile(cleanFile1, "B"); + writeFile(dirtyFile0, "C"); + writeFile(dirtyFile1, "D"); + createJournal("CLEAN k1 1 1", "DIRTY k1"); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertFalse(cleanFile0.exists()); + assertFalse(cleanFile1.exists()); + assertFalse(dirtyFile0.exists()); + assertFalse(dirtyFile1.exists()); + assertNull(cache.get("k1")); + } + + @Test public void openWithInvalidVersionClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournalWithHeader(MAGIC, "0", "100", "2", ""); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + } + + @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournalWithHeader(MAGIC, "1", "101", "2", ""); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + } + + @Test public void openWithInvalidValueCountClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournalWithHeader(MAGIC, "1", "100", "1", ""); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + } + + @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournalWithHeader(MAGIC, "1", "100", "2", "x"); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + } + + @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournal("CLEAN k1 1 1", "BOGUS"); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + assertNull(cache.get("k1")); + } + + @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournal("CLEAN k1 0000x001 1"); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + assertNull(cache.get("k1")); + } + + @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception { + cache.close(); + writeFile(getCleanFile("k1", 0), "A"); + writeFile(getCleanFile("k1", 1), "B"); + Writer writer = new FileWriter(journalFile); + writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline + writer.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertNull(cache.get("k1")); + } + + @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception { + cache.close(); + generateSomeGarbageFiles(); + createJournal("CLEAN k1 1 1 1"); + cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); + assertGarbageFilesAllDeleted(); + assertNull(cache.get("k1")); + } + + @Test public void keyWithSpaceNotPermitted() throws Exception { + try { + cache.edit("my key"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void keyWithNewlineNotPermitted() throws Exception { + try { + cache.edit("my\nkey"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void keyWithCarriageReturnNotPermitted() throws Exception { + try { + cache.edit("my\rkey"); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void nullKeyThrows() throws Exception { + try { + cache.edit(null); + fail(); + } catch (NullPointerException expected) { + } + } + + @Test public void createNewEntryWithTooFewValuesFails() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + creator.set(1, "A"); + try { + creator.commit(); + fail(); + } catch (IllegalStateException expected) { } - @After public void tearDown() throws Exception { - cache.close(); + assertFalse(getCleanFile("k1", 0).exists()); + assertFalse(getCleanFile("k1", 1).exists()); + assertFalse(getDirtyFile("k1", 0).exists()); + assertFalse(getDirtyFile("k1", 1).exists()); + assertNull(cache.get("k1")); + + DiskLruCache.Editor creator2 = cache.edit("k1"); + creator2.set(0, "B"); + creator2.set(1, "C"); + creator2.commit(); + } + + @Test public void createNewEntryWithMissingFileAborts() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + creator.set(0, "A"); + creator.set(1, "A"); + assertTrue(getDirtyFile("k1", 0).exists()); + assertTrue(getDirtyFile("k1", 1).exists()); + assertTrue(getDirtyFile("k1", 0).delete()); + assertFalse(getDirtyFile("k1", 0).exists()); + creator.commit(); // silently abort if file does not exist due to I/O issue + + assertFalse(getCleanFile("k1", 0).exists()); + assertFalse(getCleanFile("k1", 1).exists()); + assertFalse(getDirtyFile("k1", 0).exists()); + assertFalse(getDirtyFile("k1", 1).exists()); + assertNull(cache.get("k1")); + + DiskLruCache.Editor creator2 = cache.edit("k1"); + creator2.set(0, "B"); + creator2.set(1, "C"); + creator2.commit(); + } + + @Test public void revertWithTooFewValues() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + creator.set(1, "A"); + creator.abort(); + assertFalse(getCleanFile("k1", 0).exists()); + assertFalse(getCleanFile("k1", 1).exists()); + assertFalse(getDirtyFile("k1", 0).exists()); + assertFalse(getDirtyFile("k1", 1).exists()); + assertNull(cache.get("k1")); + } + + @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception { + DiskLruCache.Editor creator = cache.edit("k1"); + creator.set(0, "A"); + creator.set(1, "B"); + creator.commit(); + + DiskLruCache.Editor updater = cache.edit("k1"); + updater.set(0, "C"); + updater.commit(); + + DiskLruCache.Snapshot snapshot = cache.get("k1"); + assertEquals("C", snapshot.getString(0)); + assertEquals("B", snapshot.getString(1)); + snapshot.close(); + } + + @Test public void evictOnInsert() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + + set("A", "a", "aaa"); // size 4 + set("B", "bb", "bbbb"); // size 6 + assertEquals(10, cache.size()); + + // cause the size to grow to 12 should evict 'A' + set("C", "c", "c"); + cache.flush(); + assertEquals(8, cache.size()); + assertAbsent("A"); + assertValue("B", "bb", "bbbb"); + assertValue("C", "c", "c"); + + // causing the size to grow to 10 should evict nothing + set("D", "d", "d"); + cache.flush(); + assertEquals(10, cache.size()); + assertAbsent("A"); + assertValue("B", "bb", "bbbb"); + assertValue("C", "c", "c"); + assertValue("D", "d", "d"); + + // causing the size to grow to 18 should evict 'B' and 'C' + set("E", "eeee", "eeee"); + cache.flush(); + assertEquals(10, cache.size()); + assertAbsent("A"); + assertAbsent("B"); + assertAbsent("C"); + assertValue("D", "d", "d"); + assertValue("E", "eeee", "eeee"); + } + + @Test public void evictOnUpdate() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + + set("A", "a", "aa"); // size 3 + set("B", "b", "bb"); // size 3 + set("C", "c", "cc"); // size 3 + assertEquals(9, cache.size()); + + // causing the size to grow to 11 should evict 'A' + set("B", "b", "bbbb"); + cache.flush(); + assertEquals(8, cache.size()); + assertAbsent("A"); + assertValue("B", "b", "bbbb"); + assertValue("C", "c", "cc"); + } + + @Test public void evictionHonorsLruFromCurrentSession() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + set("A", "a", "a"); + set("B", "b", "b"); + set("C", "c", "c"); + set("D", "d", "d"); + set("E", "e", "e"); + cache.get("B").close(); // 'B' is now least recently used + + // causing the size to grow to 12 should evict 'A' + set("F", "f", "f"); + // causing the size to grow to 12 should evict 'C' + set("G", "g", "g"); + cache.flush(); + assertEquals(10, cache.size()); + assertAbsent("A"); + assertValue("B", "b", "b"); + assertAbsent("C"); + assertValue("D", "d", "d"); + assertValue("E", "e", "e"); + assertValue("F", "f", "f"); + } + + @Test public void evictionHonorsLruFromPreviousSession() throws Exception { + set("A", "a", "a"); + set("B", "b", "b"); + set("C", "c", "c"); + set("D", "d", "d"); + set("E", "e", "e"); + set("F", "f", "f"); + cache.get("B").close(); // 'B' is now least recently used + assertEquals(12, cache.size()); + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + + set("G", "g", "g"); + cache.flush(); + assertEquals(10, cache.size()); + assertAbsent("A"); + assertValue("B", "b", "b"); + assertAbsent("C"); + assertValue("D", "d", "d"); + assertValue("E", "e", "e"); + assertValue("F", "f", "f"); + assertValue("G", "g", "g"); + } + + @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + set("A", "aaaaa", "aaaaaa"); // size=11 + cache.flush(); + assertAbsent("A"); + } + + @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + set("A", "aaaaaaaaaaa", "a"); // size=12 + cache.flush(); + assertAbsent("A"); + } + + @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception { + try { + DiskLruCache.open(cacheDir, appVersion, 2, 0); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception { + try { + DiskLruCache.open(cacheDir, appVersion, 0, 10); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void removeAbsentElement() throws Exception { + cache.remove("A"); + } + + @Test public void readingTheSameStreamMultipleTimes() throws Exception { + set("A", "a", "b"); + DiskLruCache.Snapshot snapshot = cache.get("A"); + assertSame(snapshot.getInputStream(0), snapshot.getInputStream(0)); + snapshot.close(); + } + + @Test public void rebuildJournalOnRepeatedReads() throws Exception { + set("A", "a", "a"); + set("B", "b", "b"); + long lastJournalLength = 0; + while (true) { + long journalLength = journalFile.length(); + assertValue("A", "a", "a"); + assertValue("B", "b", "b"); + if (journalLength < lastJournalLength) { + System.out + .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength, + journalLength); + break; // test passed! + } + lastJournalLength = journalLength; + } + } + + @Test public void rebuildJournalOnRepeatedEdits() throws Exception { + long lastJournalLength = 0; + while (true) { + long journalLength = journalFile.length(); + set("A", "a", "a"); + set("B", "b", "b"); + if (journalLength < lastJournalLength) { + System.out + .printf("Journal compacted from %s bytes to %s bytes\n", lastJournalLength, + journalLength); + break; + } + lastJournalLength = journalLength; } - @Test public void emptyCache() throws Exception { - cache.close(); - assertJournalEquals(); + // sanity check that a rebuilt journal behaves normally + assertValue("A", "a", "a"); + assertValue("B", "b", "b"); + } + + @Test public void openCreatesDirectoryIfNecessary() throws Exception { + cache.close(); + File dir = new File(javaTmpDir, "testOpenCreatesDirectoryIfNecessary"); + cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE); + set("A", "a", "a"); + assertTrue(new File(dir, "A.0").exists()); + assertTrue(new File(dir, "A.1").exists()); + assertTrue(new File(dir, "journal").exists()); + } + + @Test public void fileDeletedExternally() throws Exception { + set("A", "a", "a"); + getCleanFile("A", 1).delete(); + assertNull(cache.get("A")); + } + + @Test public void editSameVersion() throws Exception { + set("A", "a", "a"); + DiskLruCache.Snapshot snapshot = cache.get("A"); + DiskLruCache.Editor editor = snapshot.edit(); + editor.set(1, "a2"); + editor.commit(); + assertValue("A", "a", "a2"); + } + + @Test public void editSnapshotAfterChangeAborted() throws Exception { + set("A", "a", "a"); + DiskLruCache.Snapshot snapshot = cache.get("A"); + DiskLruCache.Editor toAbort = snapshot.edit(); + toAbort.set(0, "b"); + toAbort.abort(); + DiskLruCache.Editor editor = snapshot.edit(); + editor.set(1, "a2"); + editor.commit(); + assertValue("A", "a", "a2"); + } + + @Test public void editSnapshotAfterChangeCommitted() throws Exception { + set("A", "a", "a"); + DiskLruCache.Snapshot snapshot = cache.get("A"); + DiskLruCache.Editor toAbort = snapshot.edit(); + toAbort.set(0, "b"); + toAbort.commit(); + assertNull(snapshot.edit()); + } + + @Test public void editSinceEvicted() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + set("A", "aa", "aaa"); // size 5 + DiskLruCache.Snapshot snapshot = cache.get("A"); + set("B", "bb", "bbb"); // size 5 + set("C", "cc", "ccc"); // size 5; will evict 'A' + cache.flush(); + assertNull(snapshot.edit()); + } + + @Test public void editSinceEvictedAndRecreated() throws Exception { + cache.close(); + cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); + set("A", "aa", "aaa"); // size 5 + DiskLruCache.Snapshot snapshot = cache.get("A"); + set("B", "bb", "bbb"); // size 5 + set("C", "cc", "ccc"); // size 5; will evict 'A' + set("A", "a", "aaaa"); // size 5; will evict 'B' + cache.flush(); + assertNull(snapshot.edit()); + } + + private void assertJournalEquals(String... expectedBodyLines) throws Exception { + List expectedLines = new ArrayList(); + expectedLines.add(MAGIC); + expectedLines.add(VERSION_1); + expectedLines.add("100"); + expectedLines.add("2"); + expectedLines.add(""); + expectedLines.addAll(Arrays.asList(expectedBodyLines)); + assertEquals(expectedLines, readJournalLines()); + } + + private void createJournal(String... bodyLines) throws Exception { + createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines); + } + + private void createJournalWithHeader(String magic, String version, String appVersion, + String valueCount, String blank, String... bodyLines) throws Exception { + Writer writer = new FileWriter(journalFile); + writer.write(magic + "\n"); + writer.write(version + "\n"); + writer.write(appVersion + "\n"); + writer.write(valueCount + "\n"); + writer.write(blank + "\n"); + for (String line : bodyLines) { + writer.write(line); + writer.write('\n'); } + writer.close(); + } - @Test public void writeAndReadEntry() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - creator.set(0, "ABC"); - creator.set(1, "DE"); - assertNull(creator.getString(0)); - assertNull(creator.newInputStream(0)); - assertNull(creator.getString(1)); - assertNull(creator.newInputStream(1)); - creator.commit(); - - DiskLruCache.Snapshot snapshot = cache.get("k1"); - assertEquals("ABC", snapshot.getString(0)); - assertEquals("DE", snapshot.getString(1)); + private List readJournalLines() throws Exception { + List result = new ArrayList(); + BufferedReader reader = new BufferedReader(new FileReader(journalFile)); + String line; + while ((line = reader.readLine()) != null) { + result.add(line); } + reader.close(); + return result; + } - @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - creator.set(0, "A"); - creator.set(1, "B"); - creator.commit(); - cache.close(); + private File getCleanFile(String key, int index) { + return new File(cacheDir, key + "." + index); + } - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - DiskLruCache.Snapshot snapshot = cache.get("k1"); - assertEquals("A", snapshot.getString(0)); - assertEquals("B", snapshot.getString(1)); - snapshot.close(); + private File getDirtyFile(String key, int index) { + return new File(cacheDir, key + "." + index + ".tmp"); + } + + private String readFile(File file) throws Exception { + Reader reader = new FileReader(file); + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int count; + while ((count = reader.read(buffer)) != -1) { + writer.write(buffer, 0, count); } + reader.close(); + return writer.toString(); + } - @Test public void journalWithEditAndPublish() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed - creator.set(0, "AB"); - creator.set(1, "C"); - creator.commit(); - cache.close(); - assertJournalEquals("DIRTY k1", "CLEAN k1 2 1"); + public void writeFile(File file, String content) throws Exception { + FileWriter writer = new FileWriter(file); + writer.write(content); + writer.close(); + } + + private void assertInoperable(DiskLruCache.Editor editor) throws Exception { + try { + editor.getString(0); + fail(); + } catch (IllegalStateException expected) { } - - @Test public void revertedNewFileIsRemoveInJournal() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed - creator.set(0, "AB"); - creator.set(1, "C"); - creator.abort(); - cache.close(); - assertJournalEquals("DIRTY k1", "REMOVE k1"); + try { + editor.set(0, "A"); + fail(); + } catch (IllegalStateException expected) { } - - @Test public void unterminatedEditIsRevertedOnClose() throws Exception { - cache.edit("k1"); - cache.close(); - assertJournalEquals("DIRTY k1", "REMOVE k1"); + try { + editor.newInputStream(0); + fail(); + } catch (IllegalStateException expected) { } - - @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - assertNull(cache.get("k1")); - creator.set(0, "A"); - creator.set(1, "BC"); - creator.commit(); - cache.close(); - assertJournalEquals("DIRTY k1", "CLEAN k1 1 2"); + try { + editor.newOutputStream(0); + fail(); + } catch (IllegalStateException expected) { } - - @Test public void journalWithEditAndPublishAndRead() throws Exception { - DiskLruCache.Editor k1Creator = cache.edit("k1"); - k1Creator.set(0, "AB"); - k1Creator.set(1, "C"); - k1Creator.commit(); - DiskLruCache.Editor k2Creator = cache.edit("k2"); - k2Creator.set(0, "DEF"); - k2Creator.set(1, "G"); - k2Creator.commit(); - DiskLruCache.Snapshot k1Snapshot = cache.get("k1"); - k1Snapshot.close(); - cache.close(); - assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", - "DIRTY k2", "CLEAN k2 3 1", - "READ k1"); + try { + editor.commit(); + fail(); + } catch (IllegalStateException expected) { } - - @Test public void cannotOperateOnEditAfterPublish() throws Exception { - DiskLruCache.Editor editor = cache.edit("k1"); - editor.set(0, "A"); - editor.set(1, "B"); - editor.commit(); - assertInoperable(editor); + try { + editor.abort(); + fail(); + } catch (IllegalStateException expected) { } + } - @Test public void cannotOperateOnEditAfterRevert() throws Exception { - DiskLruCache.Editor editor = cache.edit("k1"); - editor.set(0, "A"); - editor.set(1, "B"); - editor.abort(); - assertInoperable(editor); - } - - @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception { - DiskLruCache.Editor editor = cache.edit("k1"); - editor.set(0, "ABC"); - editor.set(1, "B"); - editor.commit(); - File k1 = getCleanFile("k1", 0); - assertEquals("ABC", readFile(k1)); - cache.remove("k1"); - assertFalse(k1.exists()); - } - - /** - * Each read sees a snapshot of the file at the time read was called. - * This means that two reads of the same key can see different data. - */ - @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception { - DiskLruCache.Editor v1Creator = cache.edit("k1"); - v1Creator.set(0, "AAaa"); - v1Creator.set(1, "BBbb"); - v1Creator.commit(); - - DiskLruCache.Snapshot snapshot1 = cache.get("k1"); - InputStream inV1 = snapshot1.getInputStream(0); - assertEquals('A', inV1.read()); - assertEquals('A', inV1.read()); - - DiskLruCache.Editor v1Updater = cache.edit("k1"); - v1Updater.set(0, "CCcc"); - v1Updater.set(1, "DDdd"); - v1Updater.commit(); - - DiskLruCache.Snapshot snapshot2 = cache.get("k1"); - assertEquals("CCcc", snapshot2.getString(0)); - assertEquals("DDdd", snapshot2.getString(1)); - snapshot2.close(); - - assertEquals('a', inV1.read()); - assertEquals('a', inV1.read()); - assertEquals("BBbb", snapshot1.getString(1)); - snapshot1.close(); - } - - @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception { - cache.close(); - File cleanFile0 = getCleanFile("k1", 0); - File cleanFile1 = getCleanFile("k1", 1); - File dirtyFile0 = getDirtyFile("k1", 0); - File dirtyFile1 = getDirtyFile("k1", 1); - writeFile(cleanFile0, "A"); - writeFile(cleanFile1, "B"); - writeFile(dirtyFile0, "C"); - writeFile(dirtyFile1, "D"); - createJournal("CLEAN k1 1 1", "DIRTY k1"); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertFalse(cleanFile0.exists()); - assertFalse(cleanFile1.exists()); - assertFalse(dirtyFile0.exists()); - assertFalse(dirtyFile1.exists()); - assertNull(cache.get("k1")); - } - - @Test public void openWithInvalidVersionClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournalWithHeader(MAGIC, "0", "100", "2", ""); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - } - - @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournalWithHeader(MAGIC, "1", "101", "2", ""); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - } - - @Test public void openWithInvalidValueCountClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournalWithHeader(MAGIC, "1", "100", "1", ""); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - } - - @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournalWithHeader(MAGIC, "1", "100", "2", "x"); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - } - - @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournal("CLEAN k1 1 1", "BOGUS"); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - assertNull(cache.get("k1")); - } - - @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournal("CLEAN k1 0000x001 1"); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - assertNull(cache.get("k1")); - } - - @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception { - cache.close(); - writeFile(getCleanFile("k1", 0), "A"); - writeFile(getCleanFile("k1", 1), "B"); - Writer writer = new FileWriter(journalFile); - writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline - writer.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertNull(cache.get("k1")); - } - - @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception { - cache.close(); - generateSomeGarbageFiles(); - createJournal("CLEAN k1 1 1 1"); - cache = DiskLruCache.open(cacheDir, appVersion, 2, Integer.MAX_VALUE); - assertGarbageFilesAllDeleted(); - assertNull(cache.get("k1")); - } - - @Test public void keyWithSpaceNotPermitted() throws Exception { - try { - cache.edit("my key"); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void keyWithNewlineNotPermitted() throws Exception { - try { - cache.edit("my\nkey"); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void keyWithCarriageReturnNotPermitted() throws Exception { - try { - cache.edit("my\rkey"); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void nullKeyThrows() throws Exception { - try { - cache.edit(null); - fail(); - } catch (NullPointerException expected) { - } - } - - @Test public void createNewEntryWithTooFewValuesFails() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - creator.set(1, "A"); - try { - creator.commit(); - fail(); - } catch (IllegalStateException expected) { - } - - assertFalse(getCleanFile("k1", 0).exists()); - assertFalse(getCleanFile("k1", 1).exists()); - assertFalse(getDirtyFile("k1", 0).exists()); - assertFalse(getDirtyFile("k1", 1).exists()); - assertNull(cache.get("k1")); - - DiskLruCache.Editor creator2 = cache.edit("k1"); - creator2.set(0, "B"); - creator2.set(1, "C"); - creator2.commit(); - } - - @Test public void createNewEntryWithMissingFileAborts() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - creator.set(0, "A"); - creator.set(1, "A"); - assertTrue(getDirtyFile("k1", 0).exists()); - assertTrue(getDirtyFile("k1", 1).exists()); - assertTrue(getDirtyFile("k1", 0).delete()); - assertFalse(getDirtyFile("k1", 0).exists()); - creator.commit(); // silently abort if file does not exist due to I/O issue - - assertFalse(getCleanFile("k1", 0).exists()); - assertFalse(getCleanFile("k1", 1).exists()); - assertFalse(getDirtyFile("k1", 0).exists()); - assertFalse(getDirtyFile("k1", 1).exists()); - assertNull(cache.get("k1")); - - DiskLruCache.Editor creator2 = cache.edit("k1"); - creator2.set(0, "B"); - creator2.set(1, "C"); - creator2.commit(); - } - - @Test public void revertWithTooFewValues() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - creator.set(1, "A"); - creator.abort(); - assertFalse(getCleanFile("k1", 0).exists()); - assertFalse(getCleanFile("k1", 1).exists()); - assertFalse(getDirtyFile("k1", 0).exists()); - assertFalse(getDirtyFile("k1", 1).exists()); - assertNull(cache.get("k1")); - } - - @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception { - DiskLruCache.Editor creator = cache.edit("k1"); - creator.set(0, "A"); - creator.set(1, "B"); - creator.commit(); - - DiskLruCache.Editor updater = cache.edit("k1"); - updater.set(0, "C"); - updater.commit(); - - DiskLruCache.Snapshot snapshot = cache.get("k1"); - assertEquals("C", snapshot.getString(0)); - assertEquals("B", snapshot.getString(1)); - snapshot.close(); - } - - @Test public void evictOnInsert() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - - set("A", "a", "aaa"); // size 4 - set("B", "bb", "bbbb"); // size 6 - assertEquals(10, cache.size()); - - // cause the size to grow to 12 should evict 'A' - set("C", "c", "c"); - cache.flush(); - assertEquals(8, cache.size()); - assertAbsent("A"); - assertValue("B", "bb", "bbbb"); - assertValue("C", "c", "c"); - - // causing the size to grow to 10 should evict nothing - set("D", "d", "d"); - cache.flush(); - assertEquals(10, cache.size()); - assertAbsent("A"); - assertValue("B", "bb", "bbbb"); - assertValue("C", "c", "c"); - assertValue("D", "d", "d"); - - // causing the size to grow to 18 should evict 'B' and 'C' - set("E", "eeee", "eeee"); - cache.flush(); - assertEquals(10, cache.size()); - assertAbsent("A"); - assertAbsent("B"); - assertAbsent("C"); - assertValue("D", "d", "d"); - assertValue("E", "eeee", "eeee"); - } - - @Test public void evictOnUpdate() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - - set("A", "a", "aa"); // size 3 - set("B", "b", "bb"); // size 3 - set("C", "c", "cc"); // size 3 - assertEquals(9, cache.size()); - - // causing the size to grow to 11 should evict 'A' - set("B", "b", "bbbb"); - cache.flush(); - assertEquals(8, cache.size()); - assertAbsent("A"); - assertValue("B", "b", "bbbb"); - assertValue("C", "c", "cc"); - } - - @Test public void evictionHonorsLruFromCurrentSession() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - set("A", "a", "a"); - set("B", "b", "b"); - set("C", "c", "c"); - set("D", "d", "d"); - set("E", "e", "e"); - cache.get("B").close(); // 'B' is now least recently used - - // causing the size to grow to 12 should evict 'A' - set("F", "f", "f"); - // causing the size to grow to 12 should evict 'C' - set("G", "g", "g"); - cache.flush(); - assertEquals(10, cache.size()); - assertAbsent("A"); - assertValue("B", "b", "b"); - assertAbsent("C"); - assertValue("D", "d", "d"); - assertValue("E", "e", "e"); - assertValue("F", "f", "f"); - } - - @Test public void evictionHonorsLruFromPreviousSession() throws Exception { - set("A", "a", "a"); - set("B", "b", "b"); - set("C", "c", "c"); - set("D", "d", "d"); - set("E", "e", "e"); - set("F", "f", "f"); - cache.get("B").close(); // 'B' is now least recently used - assertEquals(12, cache.size()); - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - - set("G", "g", "g"); - cache.flush(); - assertEquals(10, cache.size()); - assertAbsent("A"); - assertValue("B", "b", "b"); - assertAbsent("C"); - assertValue("D", "d", "d"); - assertValue("E", "e", "e"); - assertValue("F", "f", "f"); - assertValue("G", "g", "g"); - } - - @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - set("A", "aaaaa", "aaaaaa"); // size=11 - cache.flush(); - assertAbsent("A"); - } - - @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - set("A", "aaaaaaaaaaa", "a"); // size=12 - cache.flush(); - assertAbsent("A"); - } - - @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception { - try { - DiskLruCache.open(cacheDir, appVersion, 2, 0); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception { - try { - DiskLruCache.open(cacheDir, appVersion, 0, 10); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void removeAbsentElement() throws Exception { - cache.remove("A"); - } - - @Test public void readingTheSameStreamMultipleTimes() throws Exception { - set("A", "a", "b"); - DiskLruCache.Snapshot snapshot = cache.get("A"); - assertSame(snapshot.getInputStream(0), snapshot.getInputStream(0)); - snapshot.close(); - } - - @Test public void rebuildJournalOnRepeatedReads() throws Exception { - set("A", "a", "a"); - set("B", "b", "b"); - long lastJournalLength = 0; - while (true) { - long journalLength = journalFile.length(); - assertValue("A", "a", "a"); - assertValue("B", "b", "b"); - if (journalLength < lastJournalLength) { - System.out.printf("Journal compacted from %s bytes to %s bytes\n", - lastJournalLength, journalLength); - break; // test passed! - } - lastJournalLength = journalLength; - } - } - - @Test public void rebuildJournalOnRepeatedEdits() throws Exception { - long lastJournalLength = 0; - while (true) { - long journalLength = journalFile.length(); - set("A", "a", "a"); - set("B", "b", "b"); - if (journalLength < lastJournalLength) { - System.out.printf("Journal compacted from %s bytes to %s bytes\n", - lastJournalLength, journalLength); - break; - } - lastJournalLength = journalLength; - } - - // sanity check that a rebuilt journal behaves normally - assertValue("A", "a", "a"); - assertValue("B", "b", "b"); - } - - @Test public void openCreatesDirectoryIfNecessary() throws Exception { - cache.close(); - File dir = new File(javaTmpDir, "testOpenCreatesDirectoryIfNecessary"); - cache = DiskLruCache.open(dir, appVersion, 2, Integer.MAX_VALUE); - set("A", "a", "a"); - assertTrue(new File(dir, "A.0").exists()); - assertTrue(new File(dir, "A.1").exists()); - assertTrue(new File(dir, "journal").exists()); - } - - @Test public void fileDeletedExternally() throws Exception { - set("A", "a", "a"); - getCleanFile("A", 1).delete(); - assertNull(cache.get("A")); - } - - @Test public void editSameVersion() throws Exception { - set("A", "a", "a"); - DiskLruCache.Snapshot snapshot = cache.get("A"); - DiskLruCache.Editor editor = snapshot.edit(); - editor.set(1, "a2"); - editor.commit(); - assertValue("A", "a", "a2"); - } - - @Test public void editSnapshotAfterChangeAborted() throws Exception { - set("A", "a", "a"); - DiskLruCache.Snapshot snapshot = cache.get("A"); - DiskLruCache.Editor toAbort = snapshot.edit(); - toAbort.set(0, "b"); - toAbort.abort(); - DiskLruCache.Editor editor = snapshot.edit(); - editor.set(1, "a2"); - editor.commit(); - assertValue("A", "a", "a2"); - } - - @Test public void editSnapshotAfterChangeCommitted() throws Exception { - set("A", "a", "a"); - DiskLruCache.Snapshot snapshot = cache.get("A"); - DiskLruCache.Editor toAbort = snapshot.edit(); - toAbort.set(0, "b"); - toAbort.commit(); - assertNull(snapshot.edit()); - } - - @Test public void editSinceEvicted() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - set("A", "aa", "aaa"); // size 5 - DiskLruCache.Snapshot snapshot = cache.get("A"); - set("B", "bb", "bbb"); // size 5 - set("C", "cc", "ccc"); // size 5; will evict 'A' - cache.flush(); - assertNull(snapshot.edit()); - } - - @Test public void editSinceEvictedAndRecreated() throws Exception { - cache.close(); - cache = DiskLruCache.open(cacheDir, appVersion, 2, 10); - set("A", "aa", "aaa"); // size 5 - DiskLruCache.Snapshot snapshot = cache.get("A"); - set("B", "bb", "bbb"); // size 5 - set("C", "cc", "ccc"); // size 5; will evict 'A' - set("A", "a", "aaaa"); // size 5; will evict 'B' - cache.flush(); - assertNull(snapshot.edit()); - } - - private void assertJournalEquals(String... expectedBodyLines) throws Exception { - List expectedLines = new ArrayList(); - expectedLines.add(MAGIC); - expectedLines.add(VERSION_1); - expectedLines.add("100"); - expectedLines.add("2"); - expectedLines.add(""); - expectedLines.addAll(Arrays.asList(expectedBodyLines)); - assertEquals(expectedLines, readJournalLines()); - } - - private void createJournal(String... bodyLines) throws Exception { - createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines); - } - - private void createJournalWithHeader(String magic, String version, String appVersion, - String valueCount, String blank, String... bodyLines) throws Exception { - Writer writer = new FileWriter(journalFile); - writer.write(magic + "\n"); - writer.write(version + "\n"); - writer.write(appVersion + "\n"); - writer.write(valueCount + "\n"); - writer.write(blank + "\n"); - for (String line : bodyLines) { - writer.write(line); - writer.write('\n'); - } - writer.close(); - } - - private List readJournalLines() throws Exception { - List result = new ArrayList(); - BufferedReader reader = new BufferedReader(new FileReader(journalFile)); - String line; - while ((line = reader.readLine()) != null) { - result.add(line); - } - reader.close(); - return result; - } - - private File getCleanFile(String key, int index) { - return new File(cacheDir, key + "." + index); - } - - private File getDirtyFile(String key, int index) { - return new File(cacheDir, key + "." + index + ".tmp"); - } - - private String readFile(File file) throws Exception { - Reader reader = new FileReader(file); - StringWriter writer = new StringWriter(); - char[] buffer = new char[1024]; - int count; - while ((count = reader.read(buffer)) != -1) { - writer.write(buffer, 0, count); - } - reader.close(); - return writer.toString(); - } - - public void writeFile(File file, String content) throws Exception { - FileWriter writer = new FileWriter(file); - writer.write(content); - writer.close(); - } - - private void assertInoperable(DiskLruCache.Editor editor) throws Exception { - try { - editor.getString(0); - fail(); - } catch (IllegalStateException expected) { - } - try { - editor.set(0, "A"); - fail(); - } catch (IllegalStateException expected) { - } - try { - editor.newInputStream(0); - fail(); - } catch (IllegalStateException expected) { - } - try { - editor.newOutputStream(0); - fail(); - } catch (IllegalStateException expected) { - } - try { - editor.commit(); - fail(); - } catch (IllegalStateException expected) { - } - try { - editor.abort(); - fail(); - } catch (IllegalStateException expected) { - } - } - - private void generateSomeGarbageFiles() throws Exception { - File dir1 = new File(cacheDir, "dir1"); - File dir2 = new File(dir1, "dir2"); - writeFile(getCleanFile("g1", 0), "A"); - writeFile(getCleanFile("g1", 1), "B"); - writeFile(getCleanFile("g2", 0), "C"); - writeFile(getCleanFile("g2", 1), "D"); - writeFile(getCleanFile("g2", 1), "D"); - writeFile(new File(cacheDir, "otherFile0"), "E"); - dir1.mkdir(); - dir2.mkdir(); - writeFile(new File(dir2, "otherFile1"), "F"); - } - - private void assertGarbageFilesAllDeleted() throws Exception { - assertFalse(getCleanFile("g1", 0).exists()); - assertFalse(getCleanFile("g1", 1).exists()); - assertFalse(getCleanFile("g2", 0).exists()); - assertFalse(getCleanFile("g2", 1).exists()); - assertFalse(new File(cacheDir, "otherFile0").exists()); - assertFalse(new File(cacheDir, "dir1").exists()); - } - - private void set(String key, String value0, String value1) throws Exception { - DiskLruCache.Editor editor = cache.edit(key); - editor.set(0, value0); - editor.set(1, value1); - editor.commit(); - } - - private void assertAbsent(String key) throws Exception { - DiskLruCache.Snapshot snapshot = cache.get(key); - if (snapshot != null) { - snapshot.close(); - fail(); - } - assertFalse(getCleanFile(key, 0).exists()); - assertFalse(getCleanFile(key, 1).exists()); - assertFalse(getDirtyFile(key, 0).exists()); - assertFalse(getDirtyFile(key, 1).exists()); - } - - private void assertValue(String key, String value0, String value1) throws Exception { - DiskLruCache.Snapshot snapshot = cache.get(key); - assertEquals(value0, snapshot.getString(0)); - assertEquals(value1, snapshot.getString(1)); - assertTrue(getCleanFile(key, 0).exists()); - assertTrue(getCleanFile(key, 1).exists()); - snapshot.close(); + private void generateSomeGarbageFiles() throws Exception { + File dir1 = new File(cacheDir, "dir1"); + File dir2 = new File(dir1, "dir2"); + writeFile(getCleanFile("g1", 0), "A"); + writeFile(getCleanFile("g1", 1), "B"); + writeFile(getCleanFile("g2", 0), "C"); + writeFile(getCleanFile("g2", 1), "D"); + writeFile(getCleanFile("g2", 1), "D"); + writeFile(new File(cacheDir, "otherFile0"), "E"); + dir1.mkdir(); + dir2.mkdir(); + writeFile(new File(dir2, "otherFile1"), "F"); + } + + private void assertGarbageFilesAllDeleted() throws Exception { + assertFalse(getCleanFile("g1", 0).exists()); + assertFalse(getCleanFile("g1", 1).exists()); + assertFalse(getCleanFile("g2", 0).exists()); + assertFalse(getCleanFile("g2", 1).exists()); + assertFalse(new File(cacheDir, "otherFile0").exists()); + assertFalse(new File(cacheDir, "dir1").exists()); + } + + private void set(String key, String value0, String value1) throws Exception { + DiskLruCache.Editor editor = cache.edit(key); + editor.set(0, value0); + editor.set(1, value1); + editor.commit(); + } + + private void assertAbsent(String key) throws Exception { + DiskLruCache.Snapshot snapshot = cache.get(key); + if (snapshot != null) { + snapshot.close(); + fail(); } + assertFalse(getCleanFile(key, 0).exists()); + assertFalse(getCleanFile(key, 1).exists()); + assertFalse(getDirtyFile(key, 0).exists()); + assertFalse(getDirtyFile(key, 1).exists()); + } + + private void assertValue(String key, String value0, String value1) throws Exception { + DiskLruCache.Snapshot snapshot = cache.get(key); + assertEquals(value0, snapshot.getString(0)); + assertEquals(value1, snapshot.getString(1)); + assertTrue(getCleanFile(key, 0).exists()); + assertTrue(getCleanFile(key, 1).exists()); + snapshot.close(); + } } diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java b/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java index 4a6396ffb..9eff91919 100644 --- a/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java +++ b/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java @@ -21,29 +21,38 @@ import java.util.ArrayList; import java.util.List; public final class RecordingAuthenticator extends Authenticator { - /** base64("username:password") */ - public static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; + /** base64("username:password") */ + public static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ="; - public final List calls = new ArrayList(); - public final PasswordAuthentication authentication; + public final List calls = new ArrayList(); + public final PasswordAuthentication authentication; - public RecordingAuthenticator(PasswordAuthentication authentication) { - this.authentication = authentication; - } + public RecordingAuthenticator(PasswordAuthentication authentication) { + this.authentication = authentication; + } - public RecordingAuthenticator() { - this(new PasswordAuthentication("username", "password".toCharArray())); - } + public RecordingAuthenticator() { + this(new PasswordAuthentication("username", "password".toCharArray())); + } - @Override protected PasswordAuthentication getPasswordAuthentication() { - this.calls.add("host=" + getRequestingHost() - + " port=" + getRequestingPort() - + " site=" + getRequestingSite() - + " url=" + getRequestingURL() - + " type=" + getRequestorType() - + " prompt=" + getRequestingPrompt() - + " protocol=" + getRequestingProtocol() - + " scheme=" + getRequestingScheme()); - return authentication; - } + @Override protected PasswordAuthentication getPasswordAuthentication() { + this.calls + .add("host=" + + getRequestingHost() + + " port=" + + getRequestingPort() + + " site=" + + getRequestingSite() + + " url=" + + getRequestingURL() + + " type=" + + getRequestorType() + + " prompt=" + + getRequestingPrompt() + + " protocol=" + + getRequestingProtocol() + + " scheme=" + + getRequestingScheme()); + return authentication; + } } diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java b/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java index b172187b5..b3e2369b7 100644 --- a/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java +++ b/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java @@ -21,10 +21,10 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSession; public final class RecordingHostnameVerifier implements HostnameVerifier { - public final List calls = new ArrayList(); + public final List calls = new ArrayList(); - public boolean verify(String hostname, SSLSession session) { - calls.add("verify " + hostname); - return true; - } + public boolean verify(String hostname, SSLSession session) { + calls.add("verify " + hostname); + return true; + } } diff --git a/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java b/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java index c0a520ce8..067726354 100644 --- a/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java +++ b/src/test/java/com/squareup/okhttp/internal/SslContextBuilder.java @@ -43,84 +43,82 @@ import org.bouncycastle.x509.X509V3CertificateGenerator; * reuse SSL context instances where possible. */ public final class SslContextBuilder { - static { - Security.addProvider(new BouncyCastleProvider()); - } - - private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24; - private final String hostName; - private long notBefore = System.currentTimeMillis(); - private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS; - - /** - * @param hostName the subject of the host. For TLS this should be the - * domain name that the client uses to identify the server. - */ - public SslContextBuilder(String hostName) { - this.hostName = hostName; - } - - public SSLContext build() throws GeneralSecurityException { - char[] password = "password".toCharArray(); - - // Generate public and private keys and use them to make a self-signed certificate. - KeyPair keyPair = generateKeyPair(); - X509Certificate certificate = selfSignedCertificate(keyPair); - - // Put 'em in a key store. - KeyStore keyStore = newEmptyKeyStore(password); - Certificate[] certificateChain = { - certificate - }; - keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain); - keyStore.setCertificateEntry("cert", certificate); - - // Wrap it up in an SSL context. - KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance( - KeyManagerFactory.getDefaultAlgorithm()); - keyManagerFactory.init(keyStore, password); - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( - TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init(keyStore); - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), - new SecureRandom()); - return sslContext; - } - - private KeyPair generateKeyPair() throws GeneralSecurityException { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); - keyPairGenerator.initialize(1024, new SecureRandom()); - return keyPairGenerator.generateKeyPair(); - } - - /** - * Generates a certificate for {@code hostName} containing {@code keyPair}'s - * public key, signed by {@code keyPair}'s private key. - */ - @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies. - private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException { - X509V3CertificateGenerator generator = new X509V3CertificateGenerator(); - X500Principal issuer = new X500Principal("CN=" + hostName); - X500Principal subject = new X500Principal("CN=" + hostName); - generator.setSerialNumber(BigInteger.ONE); - generator.setIssuerDN(issuer); - generator.setNotBefore(new Date(notBefore)); - generator.setNotAfter(new Date(notAfter)); - generator.setSubjectDN(subject); - generator.setPublicKey(keyPair.getPublic()); - generator.setSignatureAlgorithm("SHA256WithRSAEncryption"); - return generator.generateX509Certificate(keyPair.getPrivate(), "BC"); - } - - private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException { - try { - KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); - InputStream in = null; // By convention, 'null' creates an empty key store. - keyStore.load(in, password); - return keyStore; - } catch (IOException e) { - throw new AssertionError(e); - } + static { + Security.addProvider(new BouncyCastleProvider()); + } + + private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24; + private final String hostName; + private long notBefore = System.currentTimeMillis(); + private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS; + + /** + * @param hostName the subject of the host. For TLS this should be the + * domain name that the client uses to identify the server. + */ + public SslContextBuilder(String hostName) { + this.hostName = hostName; + } + + public SSLContext build() throws GeneralSecurityException { + char[] password = "password".toCharArray(); + + // Generate public and private keys and use them to make a self-signed certificate. + KeyPair keyPair = generateKeyPair(); + X509Certificate certificate = selfSignedCertificate(keyPair); + + // Put 'em in a key store. + KeyStore keyStore = newEmptyKeyStore(password); + Certificate[] certificateChain = { certificate }; + keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain); + keyStore.setCertificateEntry("cert", certificate); + + // Wrap it up in an SSL context. + KeyManagerFactory keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + keyManagerFactory.init(keyStore, password); + TrustManagerFactory trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(keyStore); + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), + new SecureRandom()); + return sslContext; + } + + private KeyPair generateKeyPair() throws GeneralSecurityException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(1024, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } + + /** + * Generates a certificate for {@code hostName} containing {@code keyPair}'s + * public key, signed by {@code keyPair}'s private key. + */ + @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies. + private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException { + X509V3CertificateGenerator generator = new X509V3CertificateGenerator(); + X500Principal issuer = new X500Principal("CN=" + hostName); + X500Principal subject = new X500Principal("CN=" + hostName); + generator.setSerialNumber(BigInteger.ONE); + generator.setIssuerDN(issuer); + generator.setNotBefore(new Date(notBefore)); + generator.setNotAfter(new Date(notAfter)); + generator.setSubjectDN(subject); + generator.setPublicKey(keyPair.getPublic()); + generator.setSignatureAlgorithm("SHA256WithRSAEncryption"); + return generator.generateX509Certificate(keyPair.getPrivate(), "BC"); + } + + private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException { + try { + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + InputStream in = null; // By convention, 'null' creates an empty key store. + keyStore.load(in, password); + return keyStore; + } catch (IOException e) { + throw new AssertionError(e); } + } } diff --git a/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java b/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java index 5f85b5262..252f6ac85 100644 --- a/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java +++ b/src/test/java/com/squareup/okhttp/internal/StrictLineReaderTest.java @@ -16,60 +16,60 @@ package com.squareup.okhttp.internal; -import static com.squareup.okhttp.internal.Util.US_ASCII; import java.io.ByteArrayInputStream; import java.io.EOFException; import java.io.InputStream; -import static org.junit.Assert.fail; import org.junit.Test; -public final class StrictLineReaderTest { - @Test public void lineReaderConsistencyWithReadAsciiLine() throws Exception { - // Testing with LineReader buffer capacity 32 to check some corner cases. - StrictLineReader lineReader = new StrictLineReader(createTestInputStream(), 32, US_ASCII); - InputStream refStream = createTestInputStream(); - while (true) { - try { - String refLine = Util.readAsciiLine(refStream); - try { - String line = lineReader.readLine(); - if (!refLine.equals(line)) { - fail("line (\""+line+"\") differs from expected (\""+refLine+"\")."); - } - } catch (EOFException eof) { - fail("line reader threw EOFException too early."); - } - } catch (EOFException refEof) { - try { - lineReader.readLine(); - fail("line reader didn't throw the expected EOFException."); - } catch (EOFException eof) { - // OK - break; - } - } - } - refStream.close(); - lineReader.close(); - } +import static com.squareup.okhttp.internal.Util.US_ASCII; +import static org.junit.Assert.fail; - private InputStream createTestInputStream() { - return new ByteArrayInputStream(( - /* each source lines below should represent 32 bytes, until the next comment */ - "12 byte line\n18 byte line......\n" + - "pad\nline spanning two 32-byte bu" + - "ffers\npad......................\n" + - "pad\nline spanning three 32-byte " + - "buffers and ending with LF at th" + - "e end of a 32 byte buffer......\n" + - "pad\nLine ending with CRLF split" + - " at the end of a 32-byte buffer\r" + - "\npad...........................\n" + - /* end of 32-byte lines */ - "line ending with CRLF\r\n" + - "this is a long line with embedded CR \r ending with CRLF and having more than " + - "32 characters\r\n" + - "unterminated line - should be dropped" - ).getBytes()); +public final class StrictLineReaderTest { + @Test public void lineReaderConsistencyWithReadAsciiLine() throws Exception { + // Testing with LineReader buffer capacity 32 to check some corner cases. + StrictLineReader lineReader = new StrictLineReader(createTestInputStream(), 32, US_ASCII); + InputStream refStream = createTestInputStream(); + while (true) { + try { + String refLine = Util.readAsciiLine(refStream); + try { + String line = lineReader.readLine(); + if (!refLine.equals(line)) { + fail("line (\"" + line + "\") differs from expected (\"" + refLine + "\")."); + } + } catch (EOFException eof) { + fail("line reader threw EOFException too early."); + } + } catch (EOFException refEof) { + try { + lineReader.readLine(); + fail("line reader didn't throw the expected EOFException."); + } catch (EOFException eof) { + // OK + break; + } + } } + refStream.close(); + lineReader.close(); + } + + private InputStream createTestInputStream() { + return new ByteArrayInputStream(( + /* each source lines below should represent 32 bytes, until the next comment */ + "12 byte line\n18 byte line......\n" + + "pad\nline spanning two 32-byte bu" + + "ffers\npad......................\n" + + "pad\nline spanning three 32-byte " + + "buffers and ending with LF at th" + + "e end of a 32 byte buffer......\n" + + "pad\nLine ending with CRLF split" + + " at the end of a 32-byte buffer\r" + + "\npad...........................\n" + + /* end of 32-byte lines */ + "line ending with CRLF\r\n" + + "this is a long line with embedded CR \r ending with CRLF and having more than " + + "32 characters\r\n" + + "unterminated line - should be dropped").getBytes()); + } } diff --git a/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java b/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java index f93c4939b..11d723905 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java +++ b/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java @@ -25,24 +25,25 @@ import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; public final class ExternalSpdyExample { - public static void main(String[] args) throws Exception { - URL url = new URL("https://www.google.ca/"); - HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient().open(url); + public static void main(String[] args) throws Exception { + URL url = new URL("https://www.google.ca/"); + HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient().open(url); - connection.setHostnameVerifier(new HostnameVerifier() { - @Override public boolean verify(String s, SSLSession sslSession) { - System.out.println("VERIFYING " + s); - return true; - } - }); + connection.setHostnameVerifier(new HostnameVerifier() { + @Override public boolean verify(String s, SSLSession sslSession) { + System.out.println("VERIFYING " + s); + return true; + } + }); - int responseCode = connection.getResponseCode(); - System.out.println(responseCode); + int responseCode = connection.getResponseCode(); + System.out.println(responseCode); - BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); - String line; - while ((line = reader.readLine()) != null) { - System.out.println(line); - } + BufferedReader reader = + new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + System.out.println(line); } + } } diff --git a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java index 841647dd5..a16a720b5 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java +++ b/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java @@ -19,9 +19,7 @@ package com.squareup.okhttp.internal.http; import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; -import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.internal.http.HttpResponseCache; import com.squareup.okhttp.internal.SslContextBuilder; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; @@ -69,1803 +67,1733 @@ import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.junit.Before; -import org.junit.Test; -/** - * Android's HttpResponseCacheTest. - */ +/** Android's HttpResponseCacheTest. */ public final class HttpResponseCacheTest { - private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { - @Override public boolean verify(String s, SSLSession sslSession) { - return true; - } - }; - private final OkHttpClient client = new OkHttpClient(); - private MockWebServer server = new MockWebServer(); - private HttpResponseCache cache; - private final CookieManager cookieManager = new CookieManager(); + private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { + @Override public boolean verify(String s, SSLSession sslSession) { + return true; + } + }; + private final OkHttpClient client = new OkHttpClient(); + private MockWebServer server = new MockWebServer(); + private HttpResponseCache cache; + private final CookieManager cookieManager = new CookieManager(); - private static final SSLContext sslContext; - static { + private static final SSLContext sslContext; + static { + try { + sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Before public void setUp() throws Exception { + String tmp = System.getProperty("java.io.tmpdir"); + File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); + cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); + ResponseCache.setDefault(cache); + CookieHandler.setDefault(cookieManager); + } + + @After public void tearDown() throws Exception { + server.shutdown(); + ResponseCache.setDefault(null); + cache.getCache().delete(); + CookieHandler.setDefault(null); + } + + private HttpURLConnection openConnection(URL url) { + return client.open(url); + } + + /** + * Test that response caching is consistent with the RI and the spec. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 + */ + @Test public void responseCachingByResponseCode() throws Exception { + // Test each documented HTTP/1.1 code, plus the first unused value in each range. + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html + + // We can't test 100 because it's not really a response. + // assertCached(false, 100); + assertCached(false, 101); + assertCached(false, 102); + assertCached(true, 200); + assertCached(false, 201); + assertCached(false, 202); + assertCached(true, 203); + assertCached(false, 204); + assertCached(false, 205); + assertCached(false, 206); // we don't cache partial responses + assertCached(false, 207); + assertCached(true, 300); + assertCached(true, 301); + for (int i = 302; i <= 308; ++i) { + assertCached(false, i); + } + for (int i = 400; i <= 406; ++i) { + assertCached(false, i); + } + // (See test_responseCaching_407.) + assertCached(false, 408); + assertCached(false, 409); + // (See test_responseCaching_410.) + for (int i = 411; i <= 418; ++i) { + assertCached(false, i); + } + for (int i = 500; i <= 506; ++i) { + assertCached(false, i); + } + } + + /** + * Response code 407 should only come from proxy servers. Android's client + * throws if it is sent by an origin server. + */ + @Test public void originServerSends407() throws Exception { + server.enqueue(new MockResponse().setResponseCode(407)); + server.play(); + + URL url = server.getUrl("/"); + HttpURLConnection conn = openConnection(url); + try { + conn.getResponseCode(); + fail(); + } catch (IOException expected) { + } + } + + @Test public void responseCaching_410() throws Exception { + // the HTTP spec permits caching 410s, but the RI doesn't. + assertCached(true, 410); + } + + private void assertCached(boolean shouldPut, int responseCode) throws Exception { + server = new MockWebServer(); + MockResponse response = + new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setResponseCode(responseCode) + .setBody("ABCDE") + .addHeader("WWW-Authenticate: challenge"); + if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) { + response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\""); + } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + response.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); + } + server.enqueue(response); + server.play(); + + URL url = server.getUrl("/"); + HttpURLConnection conn = openConnection(url); + assertEquals(responseCode, conn.getResponseCode()); + + // exhaust the content stream + readAscii(conn); + + CacheResponse cached = + cache.get(url.toURI(), "GET", Collections.>emptyMap()); + if (shouldPut) { + assertNotNull(Integer.toString(responseCode), cached); + cached.getBody().close(); + } else { + assertNull(Integer.toString(responseCode), cached); + } + server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers + } + + /** + * Test that we can interrogate the response when the cache is being + * populated. http://code.google.com/p/android/issues/detail?id=7787 + */ + @Test public void responseCacheCallbackApis() throws Exception { + final String body = "ABCDE"; + final AtomicInteger cacheCount = new AtomicInteger(); + + server.enqueue( + new MockResponse().setStatus("HTTP/1.1 200 Fantastic").addHeader("fgh: ijk").setBody(body)); + server.play(); + + ResponseCache.setDefault(new ResponseCache() { + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + return null; + } + @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { + HttpURLConnection httpConnection = (HttpURLConnection) conn; try { - sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } catch (UnknownHostException e) { - throw new RuntimeException(e); + httpConnection.getRequestProperties(); + fail(); + } catch (IllegalStateException expected) { } - } - - @Before public void setUp() throws Exception { - String tmp = System.getProperty("java.io.tmpdir"); - File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); - cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); - ResponseCache.setDefault(cache); - CookieHandler.setDefault(cookieManager); - } - - @After public void tearDown() throws Exception { - server.shutdown(); - ResponseCache.setDefault(null); - cache.getCache().delete(); - CookieHandler.setDefault(null); - } - - private HttpURLConnection openConnection(URL url) { - return client.open(url); - } - - /** - * Test that response caching is consistent with the RI and the spec. - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4 - */ - @Test public void responseCachingByResponseCode() throws Exception { - // Test each documented HTTP/1.1 code, plus the first unused value in each range. - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html - - // We can't test 100 because it's not really a response. - // assertCached(false, 100); - assertCached(false, 101); - assertCached(false, 102); - assertCached(true, 200); - assertCached(false, 201); - assertCached(false, 202); - assertCached(true, 203); - assertCached(false, 204); - assertCached(false, 205); - assertCached(false, 206); // we don't cache partial responses - assertCached(false, 207); - assertCached(true, 300); - assertCached(true, 301); - for (int i = 302; i <= 308; ++i) { - assertCached(false, i); - } - for (int i = 400; i <= 406; ++i) { - assertCached(false, i); - } - // (See test_responseCaching_407.) - assertCached(false, 408); - assertCached(false, 409); - // (See test_responseCaching_410.) - for (int i = 411; i <= 418; ++i) { - assertCached(false, i); - } - for (int i = 500; i <= 506; ++i) { - assertCached(false, i); - } - } - - /** - * Response code 407 should only come from proxy servers. Android's client - * throws if it is sent by an origin server. - */ - @Test public void originServerSends407() throws Exception { - server.enqueue(new MockResponse().setResponseCode(407)); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection conn = openConnection(url); try { - conn.getResponseCode(); - fail(); + httpConnection.addRequestProperty("K", "V"); + fail(); + } catch (IllegalStateException expected) { + } + assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null)); + assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), + httpConnection.getHeaderFields().get(null)); + assertEquals(200, httpConnection.getResponseCode()); + assertEquals("Fantastic", httpConnection.getResponseMessage()); + assertEquals(body.length(), httpConnection.getContentLength()); + assertEquals("ijk", httpConnection.getHeaderField("fgh")); + try { + httpConnection.getInputStream(); // the RI doesn't forbid this, but it should + fail(); } catch (IOException expected) { } - } + cacheCount.incrementAndGet(); + return null; + } + }); - @Test public void responseCaching_410() throws Exception { - // the HTTP spec permits caching 410s, but the RI doesn't. - assertCached(true, 410); - } + URL url = server.getUrl("/"); + HttpURLConnection connection = openConnection(url); + assertEquals(body, readAscii(connection)); + assertEquals(1, cacheCount.get()); + } - private void assertCached(boolean shouldPut, int responseCode) throws Exception { - server = new MockWebServer(); - MockResponse response = new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setResponseCode(responseCode) - .setBody("ABCDE") - .addHeader("WWW-Authenticate: challenge"); - if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) { - response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\""); - } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - response.addHeader("WWW-Authenticate: Basic realm=\"protected area\""); - } - server.enqueue(response); - server.play(); + @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException { + testResponseCaching(TransferKind.FIXED_LENGTH); + } - URL url = server.getUrl("/"); - HttpURLConnection conn = openConnection(url); - assertEquals(responseCode, conn.getResponseCode()); + @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { + testResponseCaching(TransferKind.CHUNKED); + } - // exhaust the content stream - readAscii(conn); + @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException { + testResponseCaching(TransferKind.END_OF_STREAM); + } - CacheResponse cached = cache.get(url.toURI(), "GET", - Collections.>emptyMap()); - if (shouldPut) { - assertNotNull(Integer.toString(responseCode), cached); - cached.getBody().close(); - } else { - assertNull(Integer.toString(responseCode), cached); - } - server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers - } + /** + * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption + * http://code.google.com/p/android/issues/detail?id=8175 + */ + private void testResponseCaching(TransferKind transferKind) throws IOException { + MockResponse response = + new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setStatus("HTTP/1.1 200 Fantastic"); + transferKind.setBody(response, "I love puppies but hate spiders", 1); + server.enqueue(response); + server.play(); - /** - * Test that we can interrogate the response when the cache is being - * populated. http://code.google.com/p/android/issues/detail?id=7787 - */ - @Test public void responseCacheCallbackApis() throws Exception { - final String body = "ABCDE"; - final AtomicInteger cacheCount = new AtomicInteger(); + // Make sure that calling skip() doesn't omit bytes from the cache. + HttpURLConnection urlConnection = openConnection(server.getUrl("/")); + InputStream in = urlConnection.getInputStream(); + assertEquals("I love ", readAscii(urlConnection, "I love ".length())); + reliableSkip(in, "puppies but hate ".length()); + assertEquals("spiders", readAscii(urlConnection, "spiders".length())); + assertEquals(-1, in.read()); + in.close(); + assertEquals(1, cache.getWriteSuccessCount()); + assertEquals(0, cache.getWriteAbortCount()); - server.enqueue(new MockResponse() - .setStatus("HTTP/1.1 200 Fantastic") - .addHeader("fgh: ijk") - .setBody(body)); - server.play(); + urlConnection = openConnection(server.getUrl("/")); // cached! + in = urlConnection.getInputStream(); + assertEquals("I love puppies but hate spiders", + readAscii(urlConnection, "I love puppies but hate spiders".length())); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("Fantastic", urlConnection.getResponseMessage()); - ResponseCache.setDefault(new ResponseCache() { - @Override public CacheResponse get(URI uri, String requestMethod, - Map> requestHeaders) throws IOException { - return null; - } - @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { - HttpURLConnection httpConnection = (HttpURLConnection) conn; - try { - httpConnection.getRequestProperties(); - fail(); - } catch (IllegalStateException expected) { - } - try { - httpConnection.addRequestProperty("K", "V"); - fail(); - } catch (IllegalStateException expected) { - } - assertEquals("HTTP/1.1 200 Fantastic", httpConnection.getHeaderField(null)); - assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), - httpConnection.getHeaderFields().get(null)); - assertEquals(200, httpConnection.getResponseCode()); - assertEquals("Fantastic", httpConnection.getResponseMessage()); - assertEquals(body.length(), httpConnection.getContentLength()); - assertEquals("ijk", httpConnection.getHeaderField("fgh")); - try { - httpConnection.getInputStream(); // the RI doesn't forbid this, but it should - fail(); - } catch (IOException expected) { - } - cacheCount.incrementAndGet(); - return null; - } - }); + assertEquals(-1, in.read()); + in.close(); + assertEquals(1, cache.getWriteSuccessCount()); + assertEquals(0, cache.getWriteAbortCount()); + assertEquals(2, cache.getRequestCount()); + assertEquals(1, cache.getHitCount()); + } - URL url = server.getUrl("/"); - HttpURLConnection connection = openConnection(url); - assertEquals(body, readAscii(connection)); - assertEquals(1, cacheCount.get()); - } + @Test public void secureResponseCaching() throws IOException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setBody("ABC")); + server.play(); + HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/")); + connection.setSSLSocketFactory(sslContext.getSocketFactory()); + connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(connection)); - @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException { - testResponseCaching(TransferKind.FIXED_LENGTH); - } + // OpenJDK 6 fails on this line, complaining that the connection isn't open yet + String suite = connection.getCipherSuite(); + List localCerts = toListOrNull(connection.getLocalCertificates()); + List serverCerts = toListOrNull(connection.getServerCertificates()); + Principal peerPrincipal = connection.getPeerPrincipal(); + Principal localPrincipal = connection.getLocalPrincipal(); - @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException { - testResponseCaching(TransferKind.CHUNKED); - } + connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached! + connection.setSSLSocketFactory(sslContext.getSocketFactory()); + connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(connection)); - @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException { - testResponseCaching(TransferKind.END_OF_STREAM); - } + assertEquals(2, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(1, cache.getHitCount()); - /** - * HttpURLConnection.getInputStream().skip(long) causes ResponseCache corruption - * http://code.google.com/p/android/issues/detail?id=8175 - */ - private void testResponseCaching(TransferKind transferKind) throws IOException { - MockResponse response = new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setStatus("HTTP/1.1 200 Fantastic"); - transferKind.setBody(response, "I love puppies but hate spiders", 1); - server.enqueue(response); - server.play(); + assertEquals(suite, connection.getCipherSuite()); + assertEquals(localCerts, toListOrNull(connection.getLocalCertificates())); + assertEquals(serverCerts, toListOrNull(connection.getServerCertificates())); + assertEquals(peerPrincipal, connection.getPeerPrincipal()); + assertEquals(localPrincipal, connection.getLocalPrincipal()); + } - // Make sure that calling skip() doesn't omit bytes from the cache. - HttpURLConnection urlConnection = openConnection(server.getUrl("/")); - InputStream in = urlConnection.getInputStream(); - assertEquals("I love ", readAscii(urlConnection, "I love ".length())); - reliableSkip(in, "puppies but hate ".length()); - assertEquals("spiders", readAscii(urlConnection, "spiders".length())); - assertEquals(-1, in.read()); - in.close(); - assertEquals(1, cache.getWriteSuccessCount()); - assertEquals(0, cache.getWriteAbortCount()); + @Test public void cacheReturnsInsecureResponseForSecureRequest() throws IOException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("ABC")); + server.enqueue(new MockResponse().setBody("DEF")); + server.play(); - urlConnection = openConnection(server.getUrl("/")); // cached! - in = urlConnection.getInputStream(); - assertEquals("I love puppies but hate spiders", - readAscii(urlConnection, "I love puppies but hate spiders".length())); - assertEquals(200, urlConnection.getResponseCode()); - assertEquals("Fantastic", urlConnection.getResponseMessage()); + ResponseCache.setDefault(new InsecureResponseCache()); - assertEquals(-1, in.read()); - in.close(); - assertEquals(1, cache.getWriteSuccessCount()); - assertEquals(0, cache.getWriteAbortCount()); - assertEquals(2, cache.getRequestCount()); - assertEquals(1, cache.getHitCount()); - } + HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); + connection1.setSSLSocketFactory(sslContext.getSocketFactory()); + connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(connection1)); - @Test public void secureResponseCaching() throws IOException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setBody("ABC")); - server.play(); + // Not cached! + HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); + connection2.setSSLSocketFactory(sslContext.getSocketFactory()); + connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("DEF", readAscii(connection2)); + } - HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/")); - connection.setSSLSocketFactory(sslContext.getSocketFactory()); - connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(connection)); + @Test public void responseCachingAndRedirects() throws Exception { + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) + .addHeader("Location: /foo")); + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setBody("ABC")); + server.enqueue(new MockResponse().setBody("DEF")); + server.play(); - // OpenJDK 6 fails on this line, complaining that the connection isn't open yet - String suite = connection.getCipherSuite(); - List localCerts = toListOrNull(connection.getLocalCertificates()); - List serverCerts = toListOrNull(connection.getServerCertificates()); - Principal peerPrincipal = connection.getPeerPrincipal(); - Principal localPrincipal = connection.getLocalPrincipal(); + HttpURLConnection connection = openConnection(server.getUrl("/")); + assertEquals("ABC", readAscii(connection)); - connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached! - connection.setSSLSocketFactory(sslContext.getSocketFactory()); - connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(connection)); + connection = openConnection(server.getUrl("/")); // cached! + assertEquals("ABC", readAscii(connection)); - assertEquals(2, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(1, cache.getHitCount()); + assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects + assertEquals(2, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } - assertEquals(suite, connection.getCipherSuite()); - assertEquals(localCerts, toListOrNull(connection.getLocalCertificates())); - assertEquals(serverCerts, toListOrNull(connection.getServerCertificates())); - assertEquals(peerPrincipal, connection.getPeerPrincipal()); - assertEquals(localPrincipal, connection.getLocalPrincipal()); - } + @Test public void redirectToCachedResult() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("ABC")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) + .addHeader("Location: /foo")); + server.enqueue(new MockResponse().setBody("DEF")); + server.play(); - @Test public void cacheReturnsInsecureResponseForSecureRequest() throws IOException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); + assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo")))); + RecordedRequest request1 = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); + assertEquals(0, request1.getSequenceNumber()); - ResponseCache.setDefault(new InsecureResponseCache()); + assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar")))); + RecordedRequest request2 = server.takeRequest(); + assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); + assertEquals(1, request2.getSequenceNumber()); - HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); - connection1.setSSLSocketFactory(sslContext.getSocketFactory()); - connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(connection1)); + // an unrelated request should reuse the pooled connection + assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz")))); + RecordedRequest request3 = server.takeRequest(); + assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); + assertEquals(2, request3.getSequenceNumber()); + } - // Not cached! - HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); - connection2.setSSLSocketFactory(sslContext.getSocketFactory()); - connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("DEF", readAscii(connection2)); - } + @Test public void secureResponseCachingAndRedirects() throws IOException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) + .addHeader("Location: /foo")); + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .setBody("ABC")); + server.enqueue(new MockResponse().setBody("DEF")); + server.play(); - @Test public void responseCachingAndRedirects() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); + HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); + connection1.setSSLSocketFactory(sslContext.getSocketFactory()); + connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(connection1)); - HttpURLConnection connection = openConnection(server.getUrl("/")); - assertEquals("ABC", readAscii(connection)); + // Cached! + HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); + connection1.setSSLSocketFactory(sslContext.getSocketFactory()); + connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + assertEquals("ABC", readAscii(connection2)); - connection = openConnection(server.getUrl("/")); // cached! - assertEquals("ABC", readAscii(connection)); + assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 + assertEquals(2, cache.getHitCount()); + } - assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects - assertEquals(2, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } + @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException { + server.enqueue(new MockResponse().setBody("ABC")); + server.play(); - @Test public void redirectToCachedResult() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("ABC")); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); + final AtomicReference>> requestHeadersRef = + new AtomicReference>>(); + ResponseCache.setDefault(new ResponseCache() { + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + requestHeadersRef.set(requestHeaders); + return null; + } + @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { + return null; + } + }); - assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo")))); - RecordedRequest request1 = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request1.getRequestLine()); - assertEquals(0, request1.getSequenceNumber()); + URL url = server.getUrl("/"); + URLConnection urlConnection = openConnection(url); + urlConnection.addRequestProperty("A", "android"); + readAscii(urlConnection); + assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A")); + } - assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar")))); - RecordedRequest request2 = server.takeRequest(); - assertEquals("GET /bar HTTP/1.1", request2.getRequestLine()); - assertEquals(1, request2.getSequenceNumber()); + @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException { + testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); + } - // an unrelated request should reuse the pooled connection - assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz")))); - RecordedRequest request3 = server.takeRequest(); - assertEquals("GET /baz HTTP/1.1", request3.getRequestLine()); - assertEquals(2, request3.getSequenceNumber()); - } + @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException { + testServerPrematureDisconnect(TransferKind.CHUNKED); + } - @Test public void secureResponseCachingAndRedirects() throws IOException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.play(); - - HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/")); - connection1.setSSLSocketFactory(sslContext.getSocketFactory()); - connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(connection1)); - - // Cached! - HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/")); - connection1.setSSLSocketFactory(sslContext.getSocketFactory()); - connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - assertEquals("ABC", readAscii(connection2)); - - assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4 - assertEquals(2, cache.getHitCount()); - } - - @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException { - server.enqueue(new MockResponse().setBody("ABC")); - server.play(); - - final AtomicReference>> requestHeadersRef - = new AtomicReference>>(); - ResponseCache.setDefault(new ResponseCache() { - @Override public CacheResponse get(URI uri, String requestMethod, - Map> requestHeaders) throws IOException { - requestHeadersRef.set(requestHeaders); - return null; - } - @Override public CacheRequest put(URI uri, URLConnection conn) throws IOException { - return null; - } - }); - - URL url = server.getUrl("/"); - URLConnection urlConnection = openConnection(url); - urlConnection.addRequestProperty("A", "android"); - readAscii(urlConnection); - assertEquals(Arrays.asList("android"), requestHeadersRef.get().get("A")); - } - - - @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException { - testServerPrematureDisconnect(TransferKind.FIXED_LENGTH); - } - - @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException { - testServerPrematureDisconnect(TransferKind.CHUNKED); - } - - @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException { + @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException { /* * Intentionally empty. This case doesn't make sense because there's no * such thing as a premature disconnect when the disconnect itself * indicates the end of the data stream. */ + } + + private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException { + MockResponse response = new MockResponse(); + transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); + server.enqueue(truncateViolently(response, 16)); + server.enqueue(new MockResponse().setBody("Request #2")); + server.play(); + + BufferedReader reader = new BufferedReader( + new InputStreamReader(openConnection(server.getUrl("/")).getInputStream())); + assertEquals("ABCDE", reader.readLine()); + try { + reader.readLine(); + fail("This implementation silently ignored a truncated HTTP body."); + } catch (IOException expected) { + } finally { + reader.close(); } - private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException { - MockResponse response = new MockResponse(); - transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16); - server.enqueue(truncateViolently(response, 16)); - server.enqueue(new MockResponse().setBody("Request #2")); - server.play(); + assertEquals(1, cache.getWriteAbortCount()); + assertEquals(0, cache.getWriteSuccessCount()); + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("Request #2", readAscii(connection)); + assertEquals(1, cache.getWriteAbortCount()); + assertEquals(1, cache.getWriteSuccessCount()); + } - BufferedReader reader = new BufferedReader(new InputStreamReader( - openConnection(server.getUrl("/")).getInputStream())); - assertEquals("ABCDE", reader.readLine()); - try { - reader.readLine(); - fail("This implementation silently ignored a truncated HTTP body."); - } catch (IOException expected) { - } finally { - reader.close(); - } + @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException { + testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); + } - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(0, cache.getWriteSuccessCount()); - URLConnection connection = openConnection(server.getUrl("/")); - assertEquals("Request #2", readAscii(connection)); - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(1, cache.getWriteSuccessCount()); + @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException { + testClientPrematureDisconnect(TransferKind.CHUNKED); + } + + @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException { + testClientPrematureDisconnect(TransferKind.END_OF_STREAM); + } + + private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { + // Setting a low transfer speed ensures that stream discarding will time out. + MockResponse response = new MockResponse().setBytesPerSecond(6); + transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); + server.enqueue(response); + server.enqueue(new MockResponse().setBody("Request #2")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals("ABCDE", readAscii(connection, 5)); + in.close(); + try { + in.read(); + fail("Expected an IOException because the stream is closed."); + } catch (IOException expected) { } - @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException { - testClientPrematureDisconnect(TransferKind.FIXED_LENGTH); - } + assertEquals(1, cache.getWriteAbortCount()); + assertEquals(0, cache.getWriteSuccessCount()); + connection = openConnection(server.getUrl("/")); + assertEquals("Request #2", readAscii(connection)); + assertEquals(1, cache.getWriteAbortCount()); + assertEquals(1, cache.getWriteSuccessCount()); + } - @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException { - testClientPrematureDisconnect(TransferKind.CHUNKED); - } + @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception { + // last modified: 105 seconds ago + // served: 5 seconds ago + // default lifetime: (105 - 5) / 10 = 10 seconds + // expires: 10 seconds from served date = 5 seconds from now + server.enqueue( + new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) + .setBody("A")); + server.play(); - @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException { - testClientPrematureDisconnect(TransferKind.END_OF_STREAM); - } + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + URLConnection connection = openConnection(url); + assertEquals("A", readAscii(connection)); + assertNull(connection.getHeaderField("Warning")); + } - private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException { - // Setting a low transfer speed ensures that stream discarding will time out. - MockResponse response = new MockResponse().setBytesPerSecond(6); - transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024); - server.enqueue(response); - server.enqueue(new MockResponse().setBody("Request #2")); - server.play(); + @Test public void defaultExpirationDateConditionallyCached() throws Exception { + // last modified: 115 seconds ago + // served: 15 seconds ago + // default lifetime: (115 - 15) / 10 = 10 seconds + // expires: 10 seconds from served date = 5 seconds ago + String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } - URLConnection connection = openConnection(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals("ABCDE", readAscii(connection, 5)); - in.close(); - try { - in.read(); - fail("Expected an IOException because the stream is closed."); - } catch (IOException expected) { - } + @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception { + // last modified: 105 days ago + // served: 5 days ago + // default lifetime: (105 - 5) / 10 = 10 days + // expires: 10 days from served date = 5 days from now + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) + .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) + .setBody("A")); + server.play(); - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(0, cache.getWriteSuccessCount()); - connection = openConnection(server.getUrl("/")); - assertEquals("Request #2", readAscii(connection)); - assertEquals(1, cache.getWriteAbortCount()); - assertEquals(1, cache.getWriteSuccessCount()); - } + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + URLConnection connection = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection)); + assertEquals("113 HttpURLConnection \"Heuristic expiration\"", + connection.getHeaderField("Warning")); + } - @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception { - // last modified: 105 seconds ago - // served: 5 seconds ago - // default lifetime: (105 - 5) / 10 = 10 seconds - // expires: 10 seconds from served date = 5 seconds from now - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) - .setBody("A")); - server.play(); + @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception { + server.enqueue( + new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - URLConnection connection = openConnection(url); - assertEquals("A", readAscii(connection)); - assertNull(connection.getHeaderField("Warning")); - } + URL url = server.getUrl("/?foo=bar"); + assertEquals("A", readAscii(openConnection(url))); + assertEquals("B", readAscii(openConnection(url))); + } - @Test public void defaultExpirationDateConditionallyCached() throws Exception { - // last modified: 115 seconds ago - // served: 15 seconds ago - // default lifetime: (115 - 15) / 10 = 10 seconds - // expires: 10 seconds from served date = 5 seconds ago - String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS))); - List headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } + @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception { + String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } - @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception { - // last modified: 105 days ago - // served: 5 days ago - // default lifetime: (105 - 5) / 10 = 10 days - // expires: 10 days from served date = 5 days from now - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS)) - .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS)) - .setBody("A")); - server.play(); + @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception { + assertNotCached(new MockResponse().addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + } - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - URLConnection connection = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection)); - assertEquals("113 HttpURLConnection \"Heuristic expiration\"", - connection.getHeaderField("Warning")); - } + @Test public void expirationDateInTheFuture() throws Exception { + assertFullyCached(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + } - @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS)) - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); + @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception { + assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=60")); + } - URL url = server.getUrl("/?foo=bar"); - assertEquals("A", readAscii(openConnection(url))); - assertEquals("B", readAscii(openConnection(url))); - } + @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception { + String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Cache-Control: max-age=60")); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } - @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - List headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception { - assertNotCached(new MockResponse() - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - } - - @Test public void expirationDateInTheFuture() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - } - - @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=60")); - } - - @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Cache-Control: max-age=60")); - List headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { + @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception { /* * Chrome interprets max-age relative to the local clock. Both our cache * and Firefox both use the earlier of the local and server's clock. */ - assertNotCached(new MockResponse() - .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60")); - } + assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } - @Test public void maxAgeInTheFutureWithDateHeader() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=60")); - } + @Test public void maxAgeInTheFutureWithDateHeader() throws Exception { + assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=60")); + } - @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Cache-Control: max-age=60")); - } + @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception { + assertFullyCached(new MockResponse().addHeader("Cache-Control: max-age=60")); + } - @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60")); - } + @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception { + assertFullyCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } - @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60")); - } + @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception { + assertFullyCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60")); + } - @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: s-maxage=60") - .addHeader("Cache-Control: max-age=180")); - } + @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception { + assertFullyCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: s-maxage=60") + .addHeader("Cache-Control: max-age=180")); + } - @Test public void maxAgePreferredOverHigherMaxAge() throws Exception { - assertNotCached(new MockResponse() - .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: s-maxage=180") - .addHeader("Cache-Control: max-age=60")); - } + @Test public void maxAgePreferredOverHigherMaxAge() throws Exception { + assertNotCached(new MockResponse().addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: s-maxage=180") + .addHeader("Cache-Control: max-age=60")); + } - @Test public void requestMethodOptionsIsNotCached() throws Exception { - testRequestMethod("OPTIONS", false); - } + @Test public void requestMethodOptionsIsNotCached() throws Exception { + testRequestMethod("OPTIONS", false); + } - @Test public void requestMethodGetIsCached() throws Exception { - testRequestMethod("GET", true); - } + @Test public void requestMethodGetIsCached() throws Exception { + testRequestMethod("GET", true); + } - @Test public void requestMethodHeadIsNotCached() throws Exception { - // We could support this but choose not to for implementation simplicity - testRequestMethod("HEAD", false); - } + @Test public void requestMethodHeadIsNotCached() throws Exception { + // We could support this but choose not to for implementation simplicity + testRequestMethod("HEAD", false); + } - @Test public void requestMethodPostIsNotCached() throws Exception { - // We could support this but choose not to for implementation simplicity - testRequestMethod("POST", false); - } + @Test public void requestMethodPostIsNotCached() throws Exception { + // We could support this but choose not to for implementation simplicity + testRequestMethod("POST", false); + } - @Test public void requestMethodPutIsNotCached() throws Exception { - testRequestMethod("PUT", false); - } + @Test public void requestMethodPutIsNotCached() throws Exception { + testRequestMethod("PUT", false); + } - @Test public void requestMethodDeleteIsNotCached() throws Exception { - testRequestMethod("DELETE", false); - } + @Test public void requestMethodDeleteIsNotCached() throws Exception { + testRequestMethod("DELETE", false); + } - @Test public void requestMethodTraceIsNotCached() throws Exception { - testRequestMethod("TRACE", false); - } + @Test public void requestMethodTraceIsNotCached() throws Exception { + testRequestMethod("TRACE", false); + } - private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception { + private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception { /* * 1. seed the cache (potentially) * 2. expect a cache hit or miss */ - server.enqueue(new MockResponse() - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("X-Response-ID: 1")); - server.enqueue(new MockResponse() - .addHeader("X-Response-ID: 2")); - server.play(); + server.enqueue(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("X-Response-ID: 1")); + server.enqueue(new MockResponse().addHeader("X-Response-ID: 2")); + server.play(); - URL url = server.getUrl("/"); + URL url = server.getUrl("/"); - HttpURLConnection request1 = openConnection(url); - request1.setRequestMethod(requestMethod); - addRequestBodyIfNecessary(requestMethod, request1); - assertEquals("1", request1.getHeaderField("X-Response-ID")); + HttpURLConnection request1 = openConnection(url); + request1.setRequestMethod(requestMethod); + addRequestBodyIfNecessary(requestMethod, request1); + assertEquals("1", request1.getHeaderField("X-Response-ID")); - URLConnection request2 = openConnection(url); - if (expectCached) { - assertEquals("1", request1.getHeaderField("X-Response-ID")); - } else { - assertEquals("2", request2.getHeaderField("X-Response-ID")); - } + URLConnection request2 = openConnection(url); + if (expectCached) { + assertEquals("1", request1.getHeaderField("X-Response-ID")); + } else { + assertEquals("2", request2.getHeaderField("X-Response-ID")); } + } - @Test public void postInvalidatesCache() throws Exception { - testMethodInvalidates("POST"); - } + @Test public void postInvalidatesCache() throws Exception { + testMethodInvalidates("POST"); + } - @Test public void putInvalidatesCache() throws Exception { - testMethodInvalidates("PUT"); - } + @Test public void putInvalidatesCache() throws Exception { + testMethodInvalidates("PUT"); + } - @Test public void deleteMethodInvalidatesCache() throws Exception { - testMethodInvalidates("DELETE"); - } + @Test public void deleteMethodInvalidatesCache() throws Exception { + testMethodInvalidates("DELETE"); + } - private void testMethodInvalidates(String requestMethod) throws Exception { + private void testMethodInvalidates(String requestMethod) throws Exception { /* * 1. seed the cache * 2. invalidate it * 3. expect a cache miss */ - server.enqueue(new MockResponse().setBody("A") - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B")); - server.enqueue(new MockResponse().setBody("C")); - server.play(); + server.enqueue( + new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse().setBody("C")); + server.play(); - URL url = server.getUrl("/"); + URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); + assertEquals("A", readAscii(openConnection(url))); - HttpURLConnection invalidate = openConnection(url); - invalidate.setRequestMethod(requestMethod); - addRequestBodyIfNecessary(requestMethod, invalidate); - assertEquals("B", readAscii(invalidate)); + HttpURLConnection invalidate = openConnection(url); + invalidate.setRequestMethod(requestMethod); + addRequestBodyIfNecessary(requestMethod, invalidate); + assertEquals("B", readAscii(invalidate)); - assertEquals("C", readAscii(openConnection(url))); - } + assertEquals("C", readAscii(openConnection(url))); + } - @Test public void etag() throws Exception { - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("ETag: v1")); - assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); - } + @Test public void etag() throws Exception { + RecordedRequest conditionalRequest = + assertConditionallyCached(new MockResponse().addHeader("ETag: v1")); + assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1")); + } - @Test public void etagAndExpirationDateInThePast() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("ETag: v1") - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - List headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-None-Match: v1")); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } + @Test public void etagAndExpirationDateInThePast() throws Exception { + String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("ETag: v1") + .addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-None-Match: v1")); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } - @Test public void etagAndExpirationDateInTheFuture() throws Exception { - assertFullyCached(new MockResponse() - .addHeader("ETag: v1") - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - } + @Test public void etagAndExpirationDateInTheFuture() throws Exception { + assertFullyCached(new MockResponse().addHeader("ETag: v1") + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + } - @Test public void cacheControlNoCache() throws Exception { - assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); - } + @Test public void cacheControlNoCache() throws Exception { + assertNotCached(new MockResponse().addHeader("Cache-Control: no-cache")); + } - @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Cache-Control: no-cache")); - List headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } + @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception { + String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Cache-Control: no-cache")); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } - @Test public void pragmaNoCache() throws Exception { - assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); - } + @Test public void pragmaNoCache() throws Exception { + assertNotCached(new MockResponse().addHeader("Pragma: no-cache")); + } - @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception { - String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); - RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Pragma: no-cache")); - List headers = conditionalRequest.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } + @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception { + String lastModifiedDate = formatDate(-2, TimeUnit.HOURS); + RecordedRequest conditionalRequest = assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Pragma: no-cache")); + List headers = conditionalRequest.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } - @Test public void cacheControlNoStore() throws Exception { - assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); - } + @Test public void cacheControlNoStore() throws Exception { + assertNotCached(new MockResponse().addHeader("Cache-Control: no-store")); + } - @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { - assertNotCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Cache-Control: no-store")); - } + @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception { + assertNotCached(new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Cache-Control: no-store")); + } - @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception { + @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception { /* * 1. request a range * 2. request a full document, expecting a cache miss */ - server.enqueue(new MockResponse().setBody("AA") - .setResponseCode(HttpURLConnection.HTTP_PARTIAL) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) - .addHeader("Content-Range: bytes 1000-1001/2000")); - server.enqueue(new MockResponse().setBody("BB")); - server.play(); + server.enqueue(new MockResponse().setBody("AA") + .setResponseCode(HttpURLConnection.HTTP_PARTIAL) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)) + .addHeader("Content-Range: bytes 1000-1001/2000")); + server.enqueue(new MockResponse().setBody("BB")); + server.play(); - URL url = server.getUrl("/"); + URL url = server.getUrl("/"); - URLConnection range = openConnection(url); - range.addRequestProperty("Range", "bytes=1000-1001"); - assertEquals("AA", readAscii(range)); + URLConnection range = openConnection(url); + range.addRequestProperty("Range", "bytes=1000-1001"); + assertEquals("AA", readAscii(range)); - assertEquals("BB", readAscii(openConnection(url))); + assertEquals("BB", readAscii(openConnection(url))); + } + + @Test public void serverReturnsDocumentOlderThanCache() throws Exception { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + server.enqueue(new MockResponse().setBody("B") + .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); + server.play(); + + URL url = server.getUrl("/"); + + assertEquals("A", readAscii(openConnection(url))); + assertEquals("A", readAscii(openConnection(url))); + } + + @Test public void nonIdentityEncodingAndConditionalCache() throws Exception { + assertNonIdentityEncodingCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + } + + @Test public void nonIdentityEncodingAndFullCache() throws Exception { + assertNonIdentityEncodingCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + } + + private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { + server.enqueue( + response.setBody(gzip("ABCABCABC".getBytes("UTF-8"))).addHeader("Content-Encoding: gzip")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + server.play(); + assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); + assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); + } + + @Test public void expiresDateBeforeModifiedDate() throws Exception { + assertConditionallyCached( + new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); + } + + @Test public void requestMaxAge() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) + .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) + .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + server.enqueue(new MockResponse().setBody("B")); + + server.play(); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "max-age=30"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestMinFresh() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=60") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + server.enqueue(new MockResponse().setBody("B")); + + server.play(); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "min-fresh=120"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestMaxStale() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=120") + .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); + server.enqueue(new MockResponse().setBody("B")); + + server.play(); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "max-stale=180"); + assertEquals("A", readAscii(connection)); + assertEquals("110 HttpURLConnection \"Response is stale\"", + connection.getHeaderField("Warning")); + } + + @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=120, must-revalidate") + .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); + server.enqueue(new MockResponse().setBody("B")); + + server.play(); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "max-stale=180"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException { + // (no responses enqueued) + server.play(); + + HttpURLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertGatewayTimeout(connection); + } + + @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + URLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + } + + @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException { + server.enqueue(new MockResponse().setBody("A") + .addHeader("Cache-Control: max-age=30") + .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + HttpURLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertGatewayTimeout(connection); + } + + @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + HttpURLConnection connection = openConnection(server.getUrl("/")); + connection.addRequestProperty("Cache-Control", "only-if-cached"); + assertGatewayTimeout(connection); + } + + @Test public void requestCacheControlNoCache() throws Exception { + server.enqueue( + new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + URLConnection connection = openConnection(url); + connection.setRequestProperty("Cache-Control", "no-cache"); + assertEquals("B", readAscii(connection)); + } + + @Test public void requestPragmaNoCache() throws Exception { + server.enqueue( + new MockResponse().addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) + .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=60") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + URLConnection connection = openConnection(url); + connection.setRequestProperty("Pragma", "no-cache"); + assertEquals("B", readAscii(connection)); + } + + @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception { + MockResponse response = + new MockResponse().addHeader("ETag: v3").addHeader("Cache-Control: max-age=0"); + String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS); + RecordedRequest request = + assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate); + List headers = request.getHeaders(); + assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); + assertFalse(headers.contains("If-None-Match: v3")); + } + + @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { + String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); + MockResponse response = new MockResponse().addHeader("Last-Modified: " + lastModifiedDate) + .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: max-age=0"); + RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1"); + List headers = request.getHeaders(); + assertTrue(headers.contains("If-None-Match: v1")); + assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate)); + } + + private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName, + String conditionValue) throws Exception { + server.enqueue(seed.setBody("A")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + + HttpURLConnection connection = openConnection(url); + connection.addRequestProperty(conditionName, conditionValue); + assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); + assertEquals("", readAscii(connection)); + + server.takeRequest(); // seed + return server.takeRequest(); + } + + @Test public void setIfModifiedSince() throws Exception { + Date since = new Date(); + server.enqueue(new MockResponse().setBody("A")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection = openConnection(url); + connection.setIfModifiedSince(since.getTime()); + assertEquals("A", readAscii(connection)); + RecordedRequest request = server.takeRequest(); + assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since))); + } + + @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + HttpURLConnection connection = openConnection(server.getUrl("/")); + String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); + connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); + assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); + assertEquals("", readAscii(connection)); + } + + @Test public void authorizationRequestHeaderPreventsCaching() throws Exception { + server.enqueue( + new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) + .addHeader("Cache-Control: max-age=60") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection = openConnection(url); + connection.addRequestProperty("Authorization", "password"); + assertEquals("A", readAscii(connection)); + assertEquals("B", readAscii(openConnection(url))); + } + + @Test public void authorizationResponseCachedWithSMaxAge() throws Exception { + assertAuthorizationRequestFullyCached( + new MockResponse().addHeader("Cache-Control: s-maxage=60")); + } + + @Test public void authorizationResponseCachedWithPublic() throws Exception { + assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public")); + } + + @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception { + assertAuthorizationRequestFullyCached( + new MockResponse().addHeader("Cache-Control: must-revalidate")); + } + + public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception { + server.enqueue(response.addHeader("Cache-Control: max-age=60").setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection = openConnection(url); + connection.addRequestProperty("Authorization", "password"); + assertEquals("A", readAscii(connection)); + assertEquals("A", readAscii(openConnection(url))); + } + + @Test public void contentLocationDoesNotPopulateCache() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Content-Location: /bar") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/foo")))); + assertEquals("B", readAscii(openConnection(server.getUrl("/bar")))); + } + + @Test public void useCachesFalseDoesNotWriteToCache() throws Exception { + server.enqueue( + new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URLConnection connection = openConnection(server.getUrl("/")); + connection.setUseCaches(false); + assertEquals("A", readAscii(connection)); + assertEquals("B", readAscii(openConnection(server.getUrl("/")))); + } + + @Test public void useCachesFalseDoesNotReadFromCache() throws Exception { + server.enqueue( + new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + URLConnection connection = openConnection(server.getUrl("/")); + connection.setUseCaches(false); + assertEquals("B", readAscii(connection)); + } + + @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception { + URL url = new URL("http://localhost/"); + URLConnection c1 = openConnection(url); + URLConnection c2 = openConnection(url); + assertTrue(c1.getDefaultUseCaches()); + c1.setDefaultUseCaches(false); + try { + assertTrue(c1.getUseCaches()); + assertTrue(c2.getUseCaches()); + URLConnection c3 = openConnection(url); + assertFalse(c3.getUseCaches()); + } finally { + c1.setDefaultUseCaches(true); } + } - @Test public void serverReturnsDocumentOlderThanCache() throws Exception { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B") - .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS))); - server.play(); + @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse().setBody("B")); + server.play(); - URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); + assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); + assertEquals("B", readAscii(openConnection(server.getUrl("/b")))); - assertEquals("A", readAscii(openConnection(url))); - assertEquals("A", readAscii(openConnection(url))); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + @Test public void statisticsConditionalCacheMiss() throws Exception { + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.enqueue(new MockResponse().setBody("C")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertEquals("B", readAscii(openConnection(server.getUrl("/")))); + assertEquals("C", readAscii(openConnection(server.getUrl("/")))); + assertEquals(3, cache.getRequestCount()); + assertEquals(3, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + } + + @Test public void statisticsConditionalCacheHit() throws Exception { + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals(3, cache.getRequestCount()); + assertEquals(3, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } + + @Test public void statisticsFullCacheHit() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals(3, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } + + @Test public void varyMatchesChangedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + HttpURLConnection frConnection = openConnection(url); + frConnection.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(frConnection)); + + HttpURLConnection enConnection = openConnection(url); + enConnection.addRequestProperty("Accept-Language", "en-US"); + assertEquals("B", readAscii(enConnection)); + } + + @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection1 = openConnection(url); + connection1.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(connection1)); + URLConnection connection2 = openConnection(url); + connection2.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMatchesAbsentRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Foo") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + } + + @Test public void varyMatchesAddedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Foo") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + URLConnection fooConnection = openConnection(server.getUrl("/")); + fooConnection.addRequestProperty("Foo", "bar"); + assertEquals("B", readAscii(fooConnection)); + } + + @Test public void varyMatchesRemovedRequestHeaderField() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Foo") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URLConnection fooConnection = openConnection(server.getUrl("/")); + fooConnection.addRequestProperty("Foo", "bar"); + assertEquals("A", readAscii(fooConnection)); + assertEquals("B", readAscii(openConnection(server.getUrl("/")))); + } + + @Test public void varyFieldsAreCaseInsensitive() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: ACCEPT-LANGUAGE") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection1 = openConnection(url); + connection1.addRequestProperty("Accept-Language", "fr-CA"); + assertEquals("A", readAscii(connection1)); + URLConnection connection2 = openConnection(url); + connection2.addRequestProperty("accept-language", "fr-CA"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMultipleFieldsWithMatch() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language, Accept-Charset") + .addHeader("Vary: Accept-Encoding") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection1 = openConnection(url); + connection1.addRequestProperty("Accept-Language", "fr-CA"); + connection1.addRequestProperty("Accept-Charset", "UTF-8"); + connection1.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("A", readAscii(connection1)); + URLConnection connection2 = openConnection(url); + connection2.addRequestProperty("Accept-Language", "fr-CA"); + connection2.addRequestProperty("Accept-Charset", "UTF-8"); + connection2.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMultipleFieldsWithNoMatch() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language, Accept-Charset") + .addHeader("Vary: Accept-Encoding") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection frConnection = openConnection(url); + frConnection.addRequestProperty("Accept-Language", "fr-CA"); + frConnection.addRequestProperty("Accept-Charset", "UTF-8"); + frConnection.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("A", readAscii(frConnection)); + URLConnection enConnection = openConnection(url); + enConnection.addRequestProperty("Accept-Language", "en-CA"); + enConnection.addRequestProperty("Accept-Charset", "UTF-8"); + enConnection.addRequestProperty("Accept-Encoding", "identity"); + assertEquals("B", readAscii(enConnection)); + } + + @Test public void varyMultipleFieldValuesWithMatch() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection1 = openConnection(url); + connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); + connection1.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection1)); + + URLConnection connection2 = openConnection(url); + connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); + connection2.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + URLConnection connection1 = openConnection(url); + connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); + connection1.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection1)); + + URLConnection connection2 = openConnection(url); + connection2.addRequestProperty("Accept-Language", "fr-CA"); + connection2.addRequestProperty("Accept-Language", "en-US"); + assertEquals("B", readAscii(connection2)); + } + + @Test public void varyAsterisk() throws Exception { + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: *") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + assertEquals("A", readAscii(openConnection(server.getUrl("/")))); + assertEquals("B", readAscii(openConnection(server.getUrl("/")))); + } + + @Test public void varyAndHttps() throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60") + .addHeader("Vary: Accept-Language") + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url); + connection1.setSSLSocketFactory(sslContext.getSocketFactory()); + connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + connection1.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection1)); + + HttpsURLConnection connection2 = (HttpsURLConnection) client.open(url); + connection2.setSSLSocketFactory(sslContext.getSocketFactory()); + connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + connection2.addRequestProperty("Accept-Language", "en-US"); + assertEquals("A", readAscii(connection2)); + } + + @Test public void cachePlusCookies() throws Exception { + server.enqueue(new MockResponse().addHeader( + "Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().addHeader( + "Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + assertCookies(url, "a=FIRST"); + assertEquals("A", readAscii(openConnection(url))); + assertCookies(url, "a=SECOND"); + } + + @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception { + server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD") + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD, PUT") + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + URLConnection connection1 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); + + URLConnection connection2 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); + } + + @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception { + server.enqueue(new MockResponse().addHeader("Transfer-Encoding: identity") + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().addHeader("Transfer-Encoding: none") + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + URLConnection connection1 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); + + URLConnection connection2 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); + } + + @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception { + server.enqueue(new MockResponse().addHeader("Warning: 199 test danger") + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + URLConnection connection1 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("199 test danger", connection1.getHeaderField("Warning")); + + URLConnection connection2 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals(null, connection2.getHeaderField("Warning")); + } + + @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception { + server.enqueue(new MockResponse().addHeader("Warning: 299 test danger") + .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.play(); + + URLConnection connection1 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection1)); + assertEquals("299 test danger", connection1.getHeaderField("Warning")); + + URLConnection connection2 = openConnection(server.getUrl("/")); + assertEquals("A", readAscii(connection2)); + assertEquals("299 test danger", connection2.getHeaderField("Warning")); + } + + public void assertCookies(URL url, String... expectedCookies) throws Exception { + List actualCookies = new ArrayList(); + for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { + actualCookies.add(cookie.toString()); } + assertEquals(Arrays.asList(expectedCookies), actualCookies); + } - @Test public void nonIdentityEncodingAndConditionalCache() throws Exception { - assertNonIdentityEncodingCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))); + @Test public void cachePlusRange() throws Exception { + assertNotCached(new MockResponse().setResponseCode(HttpURLConnection.HTTP_PARTIAL) + .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) + .addHeader("Content-Range: bytes 100-100/200") + .addHeader("Cache-Control: max-age=60")); + } + + @Test public void conditionalHitUpdatesCache() throws Exception { + server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) + .addHeader("Cache-Control: max-age=0") + .setBody("A")); + server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=30") + .addHeader("Allow: GET, HEAD") + .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + // cache miss; seed the cache + HttpURLConnection connection1 = openConnection(server.getUrl("/a")); + assertEquals("A", readAscii(connection1)); + assertEquals(null, connection1.getHeaderField("Allow")); + + // conditional cache hit; update the cache + HttpURLConnection connection2 = openConnection(server.getUrl("/a")); + assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); + assertEquals("A", readAscii(connection2)); + assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); + + // full cache hit + HttpURLConnection connection3 = openConnection(server.getUrl("/a")); + assertEquals("A", readAscii(connection3)); + assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); + + assertEquals(2, server.getRequestCount()); + } + + /** + * @param delta the offset from the current date to use. Negative + * values yield dates in the past; positive values yield dates in the + * future. + */ + private String formatDate(long delta, TimeUnit timeUnit) { + return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta))); + } + + private String formatDate(Date date) { + DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); + rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); + return rfc1123.format(date); + } + + private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate) + throws IOException { + if (requestMethod.equals("POST") || requestMethod.equals("PUT")) { + invalidate.setDoOutput(true); + OutputStream requestBody = invalidate.getOutputStream(); + requestBody.write('x'); + requestBody.close(); } + } - @Test public void nonIdentityEncodingAndFullCache() throws Exception { - assertNonIdentityEncodingCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); + private void assertNotCached(MockResponse response) throws Exception { + server.enqueue(response.setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + assertEquals("B", readAscii(openConnection(url))); + } + + /** @return the request with the conditional get headers. */ + private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception { + // scenario 1: condition succeeds + server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK")); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + + // scenario 2: condition fails + server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK")); + server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C")); + + server.play(); + + URL valid = server.getUrl("/valid"); + HttpURLConnection connection1 = openConnection(valid); + assertEquals("A", readAscii(connection1)); + assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); + assertEquals("A-OK", connection1.getResponseMessage()); + HttpURLConnection connection2 = openConnection(valid); + assertEquals("A", readAscii(connection2)); + assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); + assertEquals("A-OK", connection2.getResponseMessage()); + + URL invalid = server.getUrl("/invalid"); + HttpURLConnection connection3 = openConnection(invalid); + assertEquals("B", readAscii(connection3)); + assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); + assertEquals("B-OK", connection3.getResponseMessage()); + HttpURLConnection connection4 = openConnection(invalid); + assertEquals("C", readAscii(connection4)); + assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); + assertEquals("C-OK", connection4.getResponseMessage()); + + server.takeRequest(); // regular get + return server.takeRequest(); // conditional get + } + + private void assertFullyCached(MockResponse response) throws Exception { + server.enqueue(response.setBody("A")); + server.enqueue(response.setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + assertEquals("A", readAscii(openConnection(url))); + assertEquals("A", readAscii(openConnection(url))); + } + + /** + * Shortens the body of {@code response} but not the corresponding headers. + * Only useful to test how clients respond to the premature conclusion of + * the HTTP body. + */ + private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { + response.setSocketPolicy(DISCONNECT_AT_END); + List headers = new ArrayList(response.getHeaders()); + response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep)); + response.getHeaders().clear(); + response.getHeaders().addAll(headers); + return response; + } + + /** + * Reads {@code count} characters from the stream. If the stream is + * exhausted before {@code count} characters can be read, the remaining + * characters are returned and the stream is closed. + */ + private String readAscii(URLConnection connection, int count) throws IOException { + HttpURLConnection httpConnection = (HttpURLConnection) connection; + InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST + ? connection.getInputStream() : httpConnection.getErrorStream(); + StringBuilder result = new StringBuilder(); + for (int i = 0; i < count; i++) { + int value = in.read(); + if (value == -1) { + in.close(); + break; + } + result.append((char) value); } + return result.toString(); + } - private void assertNonIdentityEncodingCached(MockResponse response) throws Exception { - server.enqueue(response - .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) - .addHeader("Content-Encoding: gzip")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); + private String readAscii(URLConnection connection) throws IOException { + return readAscii(connection, Integer.MAX_VALUE); + } - server.play(); - assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); - assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/")))); + private void reliableSkip(InputStream in, int length) throws IOException { + while (length > 0) { + length -= in.skip(length); } + } - @Test public void expiresDateBeforeModifiedDate() throws Exception { - assertConditionallyCached(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS))); + private void assertGatewayTimeout(HttpURLConnection connection) throws IOException { + try { + connection.getInputStream(); + fail(); + } catch (FileNotFoundException expected) { } - - @Test public void requestMaxAge() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS)) - .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)) - .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - - URLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "max-age=30"); - assertEquals("B", readAscii(connection)); - } - - @Test public void requestMinFresh() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=60") - .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - - URLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "min-fresh=120"); - assertEquals("B", readAscii(connection)); - } - - @Test public void requestMaxStale() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=120") - .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - - URLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "max-stale=180"); - assertEquals("A", readAscii(connection)); - assertEquals("110 HttpURLConnection \"Response is stale\"", - connection.getHeaderField("Warning")); - } - - @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=120, must-revalidate") - .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES))); - server.enqueue(new MockResponse().setBody("B")); - - server.play(); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - - URLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "max-stale=180"); - assertEquals("B", readAscii(connection)); - } - - @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException { - // (no responses enqueued) - server.play(); - - HttpURLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); - } - - @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=30") - .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES))); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - URLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - } - - @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A") - .addHeader("Cache-Control: max-age=30") - .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - HttpURLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); - } - - @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - HttpURLConnection connection = openConnection(server.getUrl("/")); - connection.addRequestProperty("Cache-Control", "only-if-cached"); - assertGatewayTimeout(connection); - } - - @Test public void requestCacheControlNoCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - URLConnection connection = openConnection(url); - connection.setRequestProperty("Cache-Control", "no-cache"); - assertEquals("B", readAscii(connection)); - } - - @Test public void requestPragmaNoCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS)) - .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - URLConnection connection = openConnection(url); - connection.setRequestProperty("Pragma", "no-cache"); - assertEquals("B", readAscii(connection)); - } - - @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception { - MockResponse response = new MockResponse() - .addHeader("ETag: v3") - .addHeader("Cache-Control: max-age=0"); - String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS); - RecordedRequest request = assertClientSuppliedCondition( - response, "If-Modified-Since", ifModifiedSinceDate); - List headers = request.getHeaders(); - assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate)); - assertFalse(headers.contains("If-None-Match: v3")); - } - - @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception { - String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES); - MockResponse response = new MockResponse() - .addHeader("Last-Modified: " + lastModifiedDate) - .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: max-age=0"); - RecordedRequest request = assertClientSuppliedCondition( - response, "If-None-Match", "v1"); - List headers = request.getHeaders(); - assertTrue(headers.contains("If-None-Match: v1")); - assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate)); - } - - private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName, - String conditionValue) throws Exception { - server.enqueue(seed.setBody("A")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - - HttpURLConnection connection = openConnection(url); - connection.addRequestProperty(conditionName, conditionValue); - assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); - assertEquals("", readAscii(connection)); - - server.takeRequest(); // seed - return server.takeRequest(); - } - - @Test public void setIfModifiedSince() throws Exception { - Date since = new Date(); - server.enqueue(new MockResponse().setBody("A")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection = openConnection(url); - connection.setIfModifiedSince(since.getTime()); - assertEquals("A", readAscii(connection)); - RecordedRequest request = server.takeRequest(); - assertTrue(request.getHeaders().contains("If-Modified-Since: " + formatDate(since))); - } - - @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - HttpURLConnection connection = openConnection(server.getUrl("/")); - String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS); - connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince); - assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode()); - assertEquals("", readAscii(connection)); - } - - @Test public void authorizationRequestHeaderPreventsCaching() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES)) - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection = openConnection(url); - connection.addRequestProperty("Authorization", "password"); - assertEquals("A", readAscii(connection)); - assertEquals("B", readAscii(openConnection(url))); - } - - @Test public void authorizationResponseCachedWithSMaxAge() throws Exception { - assertAuthorizationRequestFullyCached(new MockResponse() - .addHeader("Cache-Control: s-maxage=60")); - } - - @Test public void authorizationResponseCachedWithPublic() throws Exception { - assertAuthorizationRequestFullyCached(new MockResponse() - .addHeader("Cache-Control: public")); - } - - @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception { - assertAuthorizationRequestFullyCached(new MockResponse() - .addHeader("Cache-Control: must-revalidate")); - } - - public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception { - server.enqueue(response - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection = openConnection(url); - connection.addRequestProperty("Authorization", "password"); - assertEquals("A", readAscii(connection)); - assertEquals("A", readAscii(openConnection(url))); - } - - @Test public void contentLocationDoesNotPopulateCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Content-Location: /bar") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/foo")))); - assertEquals("B", readAscii(openConnection(server.getUrl("/bar")))); - } - - @Test public void useCachesFalseDoesNotWriteToCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A").setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URLConnection connection = openConnection(server.getUrl("/")); - connection.setUseCaches(false); - assertEquals("A", readAscii(connection)); - assertEquals("B", readAscii(openConnection(server.getUrl("/")))); - } - - @Test public void useCachesFalseDoesNotReadFromCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A").setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - URLConnection connection = openConnection(server.getUrl("/")); - connection.setUseCaches(false); - assertEquals("B", readAscii(connection)); - } - - @Test public void defaultUseCachesSetsInitialValueOnly() throws Exception { - URL url = new URL("http://localhost/"); - URLConnection c1 = openConnection(url); - URLConnection c2 = openConnection(url); - assertTrue(c1.getDefaultUseCaches()); - c1.setDefaultUseCaches(false); - try { - assertTrue(c1.getUseCaches()); - assertTrue(c2.getUseCaches()); - URLConnection c3 = openConnection(url); - assertFalse(c3.getUseCaches()); - } finally { - c1.setDefaultUseCaches(true); - } - } - - @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); - assertEquals("A", readAscii(openConnection(server.getUrl("/a")))); - assertEquals("B", readAscii(openConnection(server.getUrl("/b")))); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals(1, server.takeRequest().getSequenceNumber()); - assertEquals(2, server.takeRequest().getSequenceNumber()); - } - - @Test public void statisticsConditionalCacheMiss() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.enqueue(new MockResponse().setBody("C")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertEquals("B", readAscii(openConnection(server.getUrl("/")))); - assertEquals("C", readAscii(openConnection(server.getUrl("/")))); - assertEquals(3, cache.getRequestCount()); - assertEquals(3, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - } - - @Test public void statisticsConditionalCacheHit() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals(3, cache.getRequestCount()); - assertEquals(3, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } - - @Test public void statisticsFullCacheHit() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .setBody("A")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals(3, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } - - @Test public void varyMatchesChangedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection frConnection = openConnection(url); - frConnection.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(frConnection)); - - HttpURLConnection enConnection = openConnection(url); - enConnection.addRequestProperty("Accept-Language", "en-US"); - assertEquals("B", readAscii(enConnection)); - } - - @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = openConnection(url); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = openConnection(url); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection2)); - } - - @Test public void varyMatchesAbsentRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Foo") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - } - - @Test public void varyMatchesAddedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Foo") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - URLConnection fooConnection = openConnection(server.getUrl("/")); - fooConnection.addRequestProperty("Foo", "bar"); - assertEquals("B", readAscii(fooConnection)); - } - - @Test public void varyMatchesRemovedRequestHeaderField() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Foo") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URLConnection fooConnection = openConnection(server.getUrl("/")); - fooConnection.addRequestProperty("Foo", "bar"); - assertEquals("A", readAscii(fooConnection)); - assertEquals("B", readAscii(openConnection(server.getUrl("/")))); - } - - @Test public void varyFieldsAreCaseInsensitive() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: ACCEPT-LANGUAGE") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = openConnection(url); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = openConnection(url); - connection2.addRequestProperty("accept-language", "fr-CA"); - assertEquals("A", readAscii(connection2)); - } - - @Test public void varyMultipleFieldsWithMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language, Accept-Charset") - .addHeader("Vary: Accept-Encoding") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = openConnection(url); - connection1.addRequestProperty("Accept-Language", "fr-CA"); - connection1.addRequestProperty("Accept-Charset", "UTF-8"); - connection1.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(connection1)); - URLConnection connection2 = openConnection(url); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - connection2.addRequestProperty("Accept-Charset", "UTF-8"); - connection2.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(connection2)); - } - - @Test public void varyMultipleFieldsWithNoMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language, Accept-Charset") - .addHeader("Vary: Accept-Encoding") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection frConnection = openConnection(url); - frConnection.addRequestProperty("Accept-Language", "fr-CA"); - frConnection.addRequestProperty("Accept-Charset", "UTF-8"); - frConnection.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("A", readAscii(frConnection)); - URLConnection enConnection = openConnection(url); - enConnection.addRequestProperty("Accept-Language", "en-CA"); - enConnection.addRequestProperty("Accept-Charset", "UTF-8"); - enConnection.addRequestProperty("Accept-Encoding", "identity"); - assertEquals("B", readAscii(enConnection)); - } - - @Test public void varyMultipleFieldValuesWithMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = openConnection(url); - connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - URLConnection connection2 = openConnection(url); - connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection2)); - } - - @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - URLConnection connection1 = openConnection(url); - connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR"); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - URLConnection connection2 = openConnection(url); - connection2.addRequestProperty("Accept-Language", "fr-CA"); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("B", readAscii(connection2)); - } - - @Test public void varyAsterisk() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: *") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - assertEquals("A", readAscii(openConnection(server.getUrl("/")))); - assertEquals("B", readAscii(openConnection(server.getUrl("/")))); - } - - @Test public void varyAndHttps() throws Exception { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=60") - .addHeader("Vary: Accept-Language") - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url); - connection1.setSSLSocketFactory(sslContext.getSocketFactory()); - connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - connection1.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection1)); - - HttpsURLConnection connection2 = (HttpsURLConnection) client.open(url); - connection2.setSSLSocketFactory(sslContext.getSocketFactory()); - connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - connection2.addRequestProperty("Accept-Language", "en-US"); - assertEquals("A", readAscii(connection2)); - } - - @Test public void cachePlusCookies() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - assertCookies(url, "a=FIRST"); - assertEquals("A", readAscii(openConnection(url))); - assertCookies(url, "a=SECOND"); - } - - @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Allow: GET, HEAD") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Allow: GET, HEAD, PUT") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("GET, HEAD", connection1.getHeaderField("Allow")); - - URLConnection connection2 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow")); - } - - @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Transfer-Encoding: identity") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Transfer-Encoding: none") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("identity", connection1.getHeaderField("Transfer-Encoding")); - - URLConnection connection2 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals("identity", connection2.getHeaderField("Transfer-Encoding")); - } - - @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Warning: 199 test danger") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("199 test danger", connection1.getHeaderField("Warning")); - - URLConnection connection2 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals(null, connection2.getHeaderField("Warning")); - } - - @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Warning: 299 test danger") - .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.play(); - - URLConnection connection1 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection1)); - assertEquals("299 test danger", connection1.getHeaderField("Warning")); - - URLConnection connection2 = openConnection(server.getUrl("/")); - assertEquals("A", readAscii(connection2)); - assertEquals("299 test danger", connection2.getHeaderField("Warning")); - } - - public void assertCookies(URL url, String... expectedCookies) throws Exception { - List actualCookies = new ArrayList(); - for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) { - actualCookies.add(cookie.toString()); - } - assertEquals(Arrays.asList(expectedCookies), actualCookies); - } - - @Test public void cachePlusRange() throws Exception { - assertNotCached(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_PARTIAL) - .addHeader("Date: " + formatDate(0, TimeUnit.HOURS)) - .addHeader("Content-Range: bytes 100-100/200") - .addHeader("Cache-Control: max-age=60")); - } - - @Test public void conditionalHitUpdatesCache() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS)) - .addHeader("Cache-Control: max-age=0") - .setBody("A")); - server.enqueue(new MockResponse() - .addHeader("Cache-Control: max-age=30") - .addHeader("Allow: GET, HEAD") - .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - // cache miss; seed the cache - HttpURLConnection connection1 = openConnection(server.getUrl("/a")); - assertEquals("A", readAscii(connection1)); - assertEquals(null, connection1.getHeaderField("Allow")); - - // conditional cache hit; update the cache - HttpURLConnection connection2 = openConnection(server.getUrl("/a")); - assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); - assertEquals("A", readAscii(connection2)); - assertEquals("GET, HEAD", connection2.getHeaderField("Allow")); - - // full cache hit - HttpURLConnection connection3 = openConnection(server.getUrl("/a")); - assertEquals("A", readAscii(connection3)); - assertEquals("GET, HEAD", connection3.getHeaderField("Allow")); - - assertEquals(2, server.getRequestCount()); - } - - /** - * @param delta the offset from the current date to use. Negative - * values yield dates in the past; positive values yield dates in the - * future. - */ - private String formatDate(long delta, TimeUnit timeUnit) { - return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta))); - } - - private String formatDate(Date date) { - DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); - rfc1123.setTimeZone(TimeZone.getTimeZone("UTC")); - return rfc1123.format(date); - } - - private void addRequestBodyIfNecessary(String requestMethod, HttpURLConnection invalidate) - throws IOException { - if (requestMethod.equals("POST") || requestMethod.equals("PUT")) { - invalidate.setDoOutput(true); - OutputStream requestBody = invalidate.getOutputStream(); - requestBody.write('x'); - requestBody.close(); - } - } - - private void assertNotCached(MockResponse response) throws Exception { - server.enqueue(response.setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - assertEquals("B", readAscii(openConnection(url))); - } - - /** - * @return the request with the conditional get headers. - */ - private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception { - // scenario 1: condition succeeds - server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK")); - server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)); - - // scenario 2: condition fails - server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK")); - server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C")); - - server.play(); - - URL valid = server.getUrl("/valid"); - HttpURLConnection connection1 = openConnection(valid); - assertEquals("A", readAscii(connection1)); - assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode()); - assertEquals("A-OK", connection1.getResponseMessage()); - HttpURLConnection connection2 = openConnection(valid); - assertEquals("A", readAscii(connection2)); - assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode()); - assertEquals("A-OK", connection2.getResponseMessage()); - - URL invalid = server.getUrl("/invalid"); - HttpURLConnection connection3 = openConnection(invalid); - assertEquals("B", readAscii(connection3)); - assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode()); - assertEquals("B-OK", connection3.getResponseMessage()); - HttpURLConnection connection4 = openConnection(invalid); - assertEquals("C", readAscii(connection4)); - assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode()); - assertEquals("C-OK", connection4.getResponseMessage()); - - server.takeRequest(); // regular get - return server.takeRequest(); // conditional get - } - - private void assertFullyCached(MockResponse response) throws Exception { - server.enqueue(response.setBody("A")); - server.enqueue(response.setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertEquals("A", readAscii(openConnection(url))); - assertEquals("A", readAscii(openConnection(url))); - } - - /** - * Shortens the body of {@code response} but not the corresponding headers. - * Only useful to test how clients respond to the premature conclusion of - * the HTTP body. - */ - private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) { + assertEquals(504, connection.getResponseCode()); + assertEquals(-1, connection.getErrorStream().read()); + } + + enum TransferKind { + CHUNKED() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) + throws IOException { + response.setChunkedBody(content, chunkSize); + } + }, + FIXED_LENGTH() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) { + response.setBody(content); + } + }, + END_OF_STREAM() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) { + response.setBody(content); response.setSocketPolicy(DISCONNECT_AT_END); - List headers = new ArrayList(response.getHeaders()); - response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep)); - response.getHeaders().clear(); - response.getHeaders().addAll(headers); - return response; - } - - /** - * Reads {@code count} characters from the stream. If the stream is - * exhausted before {@code count} characters can be read, the remaining - * characters are returned and the stream is closed. - */ - private String readAscii(URLConnection connection, int count) throws IOException { - HttpURLConnection httpConnection = (HttpURLConnection) connection; - InputStream in = httpConnection.getResponseCode() < HttpURLConnection.HTTP_BAD_REQUEST - ? connection.getInputStream() - : httpConnection.getErrorStream(); - StringBuilder result = new StringBuilder(); - for (int i = 0; i < count; i++) { - int value = in.read(); - if (value == -1) { - in.close(); - break; - } - result.append((char) value); + for (Iterator h = response.getHeaders().iterator(); h.hasNext(); ) { + if (h.next().startsWith("Content-Length:")) { + h.remove(); + break; + } } - return result.toString(); + } + }; + + abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException; + + void setBody(MockResponse response, String content, int chunkSize) throws IOException { + setBody(response, content.getBytes("UTF-8"), chunkSize); + } + } + + private List toListOrNull(T[] arrayOrNull) { + return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; + } + + /** Returns a gzipped copy of {@code bytes}. */ + public byte[] gzip(byte[] bytes) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + OutputStream gzippedOut = new GZIPOutputStream(bytesOut); + gzippedOut.write(bytes); + gzippedOut.close(); + return bytesOut.toByteArray(); + } + + private class InsecureResponseCache extends ResponseCache { + @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { + return cache.put(uri, connection); } - private String readAscii(URLConnection connection) throws IOException { - return readAscii(connection, Integer.MAX_VALUE); - } - - private void reliableSkip(InputStream in, int length) throws IOException { - while (length > 0) { - length -= in.skip(length); - } - } - - private void assertGatewayTimeout(HttpURLConnection connection) throws IOException { - try { - connection.getInputStream(); - fail(); - } catch (FileNotFoundException expected) { - } - assertEquals(504, connection.getResponseCode()); - assertEquals(-1, connection.getErrorStream().read()); - } - - enum TransferKind { - CHUNKED() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) - throws IOException { - response.setChunkedBody(content, chunkSize); - } - }, - FIXED_LENGTH() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) { - response.setBody(content); - } - }, - END_OF_STREAM() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) { - response.setBody(content); - response.setSocketPolicy(DISCONNECT_AT_END); - for (Iterator h = response.getHeaders().iterator(); h.hasNext(); ) { - if (h.next().startsWith("Content-Length:")) { - h.remove(); - break; - } - } - } + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + final CacheResponse response = cache.get(uri, requestMethod, requestHeaders); + if (response instanceof SecureCacheResponse) { + return new CacheResponse() { + @Override public InputStream getBody() throws IOException { + return response.getBody(); + } + @Override public Map> getHeaders() throws IOException { + return response.getHeaders(); + } }; - - abstract void setBody(MockResponse response, byte[] content, int chunkSize) - throws IOException; - - void setBody(MockResponse response, String content, int chunkSize) throws IOException { - setBody(response, content.getBytes("UTF-8"), chunkSize); - } - } - - private List toListOrNull(T[] arrayOrNull) { - return arrayOrNull != null ? Arrays.asList(arrayOrNull) : null; - } - - /** - * Returns a gzipped copy of {@code bytes}. - */ - public byte[] gzip(byte[] bytes) throws IOException { - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - OutputStream gzippedOut = new GZIPOutputStream(bytesOut); - gzippedOut.write(bytes); - gzippedOut.close(); - return bytesOut.toByteArray(); - } - - private class InsecureResponseCache extends ResponseCache { - @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { - return cache.put(uri, connection); - } - - @Override public CacheResponse get(URI uri, String requestMethod, - Map> requestHeaders) throws IOException { - final CacheResponse response = cache.get(uri, requestMethod, requestHeaders); - if (response instanceof SecureCacheResponse) { - return new CacheResponse() { - @Override public InputStream getBody() throws IOException { - return response.getBody(); - } - @Override public Map> getHeaders() throws IOException { - return response.getHeaders(); - } - }; - } - return response; - } + } + return response; } + } } 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 243561cc2..32649cf20 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java +++ b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java @@ -17,55 +17,48 @@ package com.squareup.okhttp.internal.http; import java.util.Arrays; import java.util.List; -import static org.junit.Assert.assertEquals; import org.junit.Test; +import static org.junit.Assert.assertEquals; + public final class RawHeadersTest { - @Test public void parseNameValueBlock() { - List nameValueBlock = Arrays.asList( - "cache-control", - "no-cache, no-store", - "set-cookie", - "Cookie1\u0000Cookie2", - ":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("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("200 OK", rawHeaders.getValue(3)); - } + @Test public void parseNameValueBlock() { + List nameValueBlock = + Arrays.asList("cache-control", "no-cache, no-store", "set-cookie", "Cookie1\u0000Cookie2", + ":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("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("200 OK", rawHeaders.getValue(3)); + } - @Test public void toNameValueBlock() { - RawHeaders rawHeaders = new RawHeaders(); - rawHeaders.add("cache-control", "no-cache, no-store"); - rawHeaders.add("set-cookie", "Cookie1"); - rawHeaders.add("set-cookie", "Cookie2"); - 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" - ); - assertEquals(expected, nameValueBlock); - } + @Test public void toNameValueBlock() { + RawHeaders rawHeaders = new RawHeaders(); + rawHeaders.add("cache-control", "no-cache, no-store"); + rawHeaders.add("set-cookie", "Cookie1"); + rawHeaders.add("set-cookie", "Cookie2"); + 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"); + 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()); - } + @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/http/RouteSelectorTest.java b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java index 14642e950..4dc2e329d 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java +++ b/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Proxy; -import static java.net.Proxy.NO_PROXY; import java.net.ProxySelector; import java.net.SocketAddress; import java.net.URI; @@ -36,338 +35,321 @@ import java.util.NoSuchElementException; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; +import org.junit.Test; + +import static java.net.Proxy.NO_PROXY; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.junit.Test; public final class RouteSelectorTest { - private static final int proxyAPort = 1001; - private static final String proxyAHost = "proxyA"; - private static final Proxy proxyA - = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort)); - private static final int proxyBPort = 1002; - private static final String proxyBHost = "proxyB"; - private static final Proxy proxyB - = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort)); - private static final URI uri; - private static final String uriHost = "hostA"; - private static final int uriPort = 80; + private static final int proxyAPort = 1001; + private static final String proxyAHost = "proxyA"; + private static final Proxy proxyA = + new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort)); + private static final int proxyBPort = 1002; + private static final String proxyBHost = "proxyB"; + private static final Proxy proxyB = + new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort)); + private static final URI uri; + private static final String uriHost = "hostA"; + private static final int uriPort = 80; - private static final SSLContext sslContext; - private static final SSLSocketFactory socketFactory; - private static final HostnameVerifier hostnameVerifier; - private static final ConnectionPool pool; - static { - try { - uri = new URI("http://" + uriHost + ":" + uriPort + "/path"); - sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); - socketFactory = sslContext.getSocketFactory(); - pool = ConnectionPool.getDefault(); - hostnameVerifier = HttpsURLConnectionImpl.getDefaultHostnameVerifier(); - } catch (Exception e) { - throw new AssertionError(e); - } + private static final SSLContext sslContext; + private static final SSLSocketFactory socketFactory; + private static final HostnameVerifier hostnameVerifier; + private static final ConnectionPool pool; + static { + try { + uri = new URI("http://" + uriHost + ":" + uriPort + "/path"); + sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); + socketFactory = sslContext.getSocketFactory(); + pool = ConnectionPool.getDefault(); + hostnameVerifier = HttpsURLConnectionImpl.getDefaultHostnameVerifier(); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + private final FakeDns dns = new FakeDns(); + private final FakeProxySelector proxySelector = new FakeProxySelector(); + + @Test public void singleRoute() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, null); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 1); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + dns.assertRequests(uriHost); + + assertFalse(routeSelector.hasNext()); + try { + routeSelector.next(); + fail(); + } catch (NoSuchElementException expected) { + } + } + + @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, proxyA); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 2); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, + false); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort, + false); + + assertFalse(routeSelector.hasNext()); + dns.assertRequests(proxyAHost); + proxySelector.assertRequests(); // No proxy selector requests! + } + + @Test public void explicitDirectProxy() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, NO_PROXY); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 2); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, false); + + assertFalse(routeSelector.hasNext()); + dns.assertRequests(uri.getHost()); + proxySelector.assertRequests(); // No proxy selector requests! + } + + @Test public void proxySelectorReturnsNull() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, null); + + proxySelector.proxies = null; + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + proxySelector.assertRequests(uri); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 1); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + dns.assertRequests(uriHost); + + assertFalse(routeSelector.hasNext()); + } + + @Test public void proxySelectorReturnsNoProxies() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, null); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 2); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, false); + + assertFalse(routeSelector.hasNext()); + dns.assertRequests(uri.getHost()); + proxySelector.assertRequests(uri); + } + + @Test public void proxySelectorReturnsMultipleProxies() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, null); + + proxySelector.proxies.add(proxyA); + proxySelector.proxies.add(proxyB); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + proxySelector.assertRequests(uri); + + // First try the IP addresses of the first proxy, in sequence. + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 2); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, + false); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort, + false); + dns.assertRequests(proxyAHost); + + // Next try the IP address of the second proxy. + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(254, 1); + assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort, + false); + dns.assertRequests(proxyBHost); + + // Finally try the only IP address of the origin server. + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(253, 1); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + dns.assertRequests(uriHost); + + assertFalse(routeSelector.hasNext()); + } + + @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, null); + + proxySelector.proxies.add(NO_PROXY); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + proxySelector.assertRequests(uri); + + // Only the origin server will be attempted. + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 1); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + dns.assertRequests(uriHost); + + assertFalse(routeSelector.hasNext()); + } + + @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception { + Address address = new Address(uriHost, uriPort, null, null, null); + + proxySelector.proxies.add(proxyA); + proxySelector.proxies.add(proxyB); + proxySelector.proxies.add(proxyA); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + proxySelector.assertRequests(uri); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 1); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, + false); + dns.assertRequests(proxyAHost); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = null; + try { + routeSelector.next(); + fail(); + } catch (UnknownHostException expected) { + } + dns.assertRequests(proxyBHost); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 1); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, + false); + dns.assertRequests(proxyAHost); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(254, 1); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + dns.assertRequests(uriHost); + + assertFalse(routeSelector.hasNext()); + } + + @Test public void multipleTlsModes() throws Exception { + Address address = + new Address(uriHost, uriPort, socketFactory, hostnameVerifier, Proxy.NO_PROXY); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + + assertTrue(routeSelector.hasNext()); + dns.inetAddresses = makeFakeAddresses(255, 1); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, true); + dns.assertRequests(uriHost); + + assertTrue(routeSelector.hasNext()); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + dns.assertRequests(); // No more DNS requests since the previous! + + assertFalse(routeSelector.hasNext()); + } + + @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception { + Address address = new Address(uriHost, uriPort, socketFactory, hostnameVerifier, null); + proxySelector.proxies.add(proxyA); + proxySelector.proxies.add(proxyB); + RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + + // Proxy A + dns.inetAddresses = makeFakeAddresses(255, 2); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, true); + dns.assertRequests(proxyAHost); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort, + false); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort, true); + assertConnection(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort, + false); + + // Proxy B + dns.inetAddresses = makeFakeAddresses(254, 2); + assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort, true); + dns.assertRequests(proxyBHost); + assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort, + false); + assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort, true); + assertConnection(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort, + false); + + // Origin + dns.inetAddresses = makeFakeAddresses(253, 2); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, true); + dns.assertRequests(uriHost); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort, false); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, true); + assertConnection(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort, false); + + assertFalse(routeSelector.hasNext()); + } + + private void assertConnection(Connection connection, Address address, Proxy proxy, + InetAddress socketAddress, int socketPort, boolean modernTls) { + assertEquals(address, connection.getAddress()); + assertEquals(proxy, connection.getProxy()); + assertEquals(socketAddress, connection.getSocketAddress().getAddress()); + assertEquals(socketPort, connection.getSocketAddress().getPort()); + assertEquals(modernTls, connection.isModernTls()); + } + + private static InetAddress[] makeFakeAddresses(int prefix, int count) { + try { + InetAddress[] result = new InetAddress[count]; + for (int i = 0; i < count; i++) { + result[i] = + InetAddress.getByAddress(new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i }); + } + return result; + } catch (UnknownHostException e) { + throw new AssertionError(); + } + } + + private static class FakeDns implements Dns { + List requestedHosts = new ArrayList(); + InetAddress[] inetAddresses; + + @Override public InetAddress[] getAllByName(String host) throws UnknownHostException { + requestedHosts.add(host); + if (inetAddresses == null) throw new UnknownHostException(); + return inetAddresses; } - private final FakeDns dns = new FakeDns(); - private final FakeProxySelector proxySelector = new FakeProxySelector(); + public void assertRequests(String... expectedHosts) { + assertEquals(Arrays.asList(expectedHosts), requestedHosts); + requestedHosts.clear(); + } + } - @Test public void singleRoute() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, null); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); + private static class FakeProxySelector extends ProxySelector { + List requestedUris = new ArrayList(); + List proxies = new ArrayList(); + List failures = new ArrayList(); - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 1); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - dns.assertRequests(uriHost); - - assertFalse(routeSelector.hasNext()); - try { - routeSelector.next(); - fail(); - } catch (NoSuchElementException expected) { - } + @Override public List select(URI uri) { + requestedUris.add(uri); + return proxies; } - @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, proxyA); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 2); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[0], proxyAPort, false); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[1], proxyAPort, false); - - assertFalse(routeSelector.hasNext()); - dns.assertRequests(proxyAHost); - proxySelector.assertRequests(); // No proxy selector requests! + public void assertRequests(URI... expectedUris) { + assertEquals(Arrays.asList(expectedUris), requestedUris); + requestedUris.clear(); } - @Test public void explicitDirectProxy() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, NO_PROXY); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 2); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[1], uriPort, false); - - assertFalse(routeSelector.hasNext()); - dns.assertRequests(uri.getHost()); - proxySelector.assertRequests(); // No proxy selector requests! - } - - @Test public void proxySelectorReturnsNull() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, null); - - proxySelector.proxies = null; - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - proxySelector.assertRequests(uri); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 1); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - dns.assertRequests(uriHost); - - assertFalse(routeSelector.hasNext()); - } - - @Test public void proxySelectorReturnsNoProxies() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, null); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 2); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[1], uriPort, false); - - assertFalse(routeSelector.hasNext()); - dns.assertRequests(uri.getHost()); - proxySelector.assertRequests(uri); - } - - @Test public void proxySelectorReturnsMultipleProxies() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, null); - - proxySelector.proxies.add(proxyA); - proxySelector.proxies.add(proxyB); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - proxySelector.assertRequests(uri); - - // First try the IP addresses of the first proxy, in sequence. - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 2); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[0], proxyAPort, false); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[1], proxyAPort, false); - dns.assertRequests(proxyAHost); - - // Next try the IP address of the second proxy. - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(254, 1); - assertConnection(routeSelector.next(), - address, proxyB, dns.inetAddresses[0], proxyBPort, false); - dns.assertRequests(proxyBHost); - - // Finally try the only IP address of the origin server. - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(253, 1); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - dns.assertRequests(uriHost); - - assertFalse(routeSelector.hasNext()); - } - - @Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, null); - - proxySelector.proxies.add(NO_PROXY); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - proxySelector.assertRequests(uri); - - // Only the origin server will be attempted. - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 1); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - dns.assertRequests(uriHost); - - assertFalse(routeSelector.hasNext()); - } - - @Test public void proxyDnsFailureContinuesToNextProxy() throws Exception { - Address address = new Address(uriHost, uriPort, null, null, null); - - proxySelector.proxies.add(proxyA); - proxySelector.proxies.add(proxyB); - proxySelector.proxies.add(proxyA); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - proxySelector.assertRequests(uri); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 1); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[0], proxyAPort, false); - dns.assertRequests(proxyAHost); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = null; - try { - routeSelector.next(); - fail(); - } catch (UnknownHostException expected) { - } - dns.assertRequests(proxyBHost); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 1); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[0], proxyAPort, false); - dns.assertRequests(proxyAHost); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(254, 1); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - dns.assertRequests(uriHost); - - assertFalse(routeSelector.hasNext()); - } - - @Test public void multipleTlsModes() throws Exception { - Address address = new Address( - uriHost, uriPort, socketFactory, hostnameVerifier, Proxy.NO_PROXY); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - - assertTrue(routeSelector.hasNext()); - dns.inetAddresses = makeFakeAddresses(255, 1); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, true); - dns.assertRequests(uriHost); - - assertTrue(routeSelector.hasNext()); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - dns.assertRequests(); // No more DNS requests since the previous! - - assertFalse(routeSelector.hasNext()); - } - - @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception { - Address address = new Address( - uriHost, uriPort, socketFactory, hostnameVerifier, null); - proxySelector.proxies.add(proxyA); - proxySelector.proxies.add(proxyB); - RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns); - - // Proxy A - dns.inetAddresses = makeFakeAddresses(255, 2); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[0], proxyAPort, true); - dns.assertRequests(proxyAHost); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[0], proxyAPort, false); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[1], proxyAPort, true); - assertConnection(routeSelector.next(), - address, proxyA, dns.inetAddresses[1], proxyAPort, false); - - // Proxy B - dns.inetAddresses = makeFakeAddresses(254, 2); - assertConnection(routeSelector.next(), - address, proxyB, dns.inetAddresses[0], proxyBPort, true); - dns.assertRequests(proxyBHost); - assertConnection(routeSelector.next(), - address, proxyB, dns.inetAddresses[0], proxyBPort, false); - assertConnection(routeSelector.next(), - address, proxyB, dns.inetAddresses[1], proxyBPort, true); - assertConnection(routeSelector.next(), - address, proxyB, dns.inetAddresses[1], proxyBPort, false); - - // Origin - dns.inetAddresses = makeFakeAddresses(253, 2); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, true); - dns.assertRequests(uriHost); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[0], uriPort, false); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[1], uriPort, true); - assertConnection(routeSelector.next(), - address, NO_PROXY, dns.inetAddresses[1], uriPort, false); - - assertFalse(routeSelector.hasNext()); - } - - private void assertConnection(Connection connection, Address address, - Proxy proxy, InetAddress socketAddress, int socketPort, boolean modernTls) { - assertEquals(address, connection.getAddress()); - assertEquals(proxy, connection.getProxy()); - assertEquals(socketAddress, connection.getSocketAddress().getAddress()); - assertEquals(socketPort, connection.getSocketAddress().getPort()); - assertEquals(modernTls, connection.isModernTls()); - } - - private static InetAddress[] makeFakeAddresses(int prefix, int count) { - try { - InetAddress[] result = new InetAddress[count]; - for (int i = 0; i < count; i++) { - result[i] = InetAddress.getByAddress( - new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i }); - } - return result; - } catch (UnknownHostException e) { - throw new AssertionError(); - } - } - - private static class FakeDns implements Dns { - List requestedHosts = new ArrayList(); - InetAddress[] inetAddresses; - - @Override public InetAddress[] getAllByName(String host) throws UnknownHostException { - requestedHosts.add(host); - if (inetAddresses == null) throw new UnknownHostException(); - return inetAddresses; - } - - public void assertRequests(String... expectedHosts) { - assertEquals(Arrays.asList(expectedHosts), requestedHosts); - requestedHosts.clear(); - } - } - - private static class FakeProxySelector extends ProxySelector { - List requestedUris = new ArrayList(); - List proxies = new ArrayList(); - List failures = new ArrayList(); - - @Override public List select(URI uri) { - requestedUris.add(uri); - return proxies; - } - - public void assertRequests(URI... expectedUris) { - assertEquals(Arrays.asList(expectedUris), requestedUris); - requestedUris.clear(); - } - - @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { - InetSocketAddress socketAddress = (InetSocketAddress) sa; - failures.add(String.format("%s %s:%d %s", uri, socketAddress.getHostName(), - socketAddress.getPort(), ioe.getMessage())); - } + @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + InetSocketAddress socketAddress = (InetSocketAddress) sa; + failures.add( + String.format("%s %s:%d %s", uri, socketAddress.getHostName(), socketAddress.getPort(), + ioe.getMessage())); } + } } diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java index 6e41b9da5..ee702f3b9 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java +++ b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java @@ -20,10 +20,6 @@ import com.google.mockwebserver.MockResponse; import com.google.mockwebserver.MockWebServer; import com.google.mockwebserver.RecordedRequest; import com.google.mockwebserver.SocketPolicy; -import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; -import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; -import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; -import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; import com.squareup.okhttp.OkHttpClient; import com.squareup.okhttp.internal.RecordingAuthenticator; import com.squareup.okhttp.internal.RecordingHostnameVerifier; @@ -72,2336 +68,2277 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_END; +import static com.google.mockwebserver.SocketPolicy.DISCONNECT_AT_START; +import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_INPUT_AT_END; +import static com.google.mockwebserver.SocketPolicy.SHUTDOWN_OUTPUT_AT_END; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -/** - * Android's URLConnectionTest. - */ +/** Android's URLConnectionTest. */ public final class URLConnectionTest { - private MockWebServer server = new MockWebServer(); - private MockWebServer server2 = new MockWebServer(); - - private final OkHttpClient client = new OkHttpClient(); - private HttpResponseCache cache; - private String hostName; - - private static final SSLContext sslContext; - static { - try { - sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } - } - - @Before public void setUp() throws Exception { - hostName = server.getHostName(); - } - - @After public void tearDown() throws Exception { - System.clearProperty("proxyHost"); - System.clearProperty("proxyPort"); - System.clearProperty("http.proxyHost"); - System.clearProperty("http.proxyPort"); - System.clearProperty("https.proxyHost"); - System.clearProperty("https.proxyPort"); - server.shutdown(); - server2.shutdown(); - if (cache != null) { - cache.getCache().delete(); - } - } - - @Test public void requestHeaders() throws IOException, InterruptedException { - server.enqueue(new MockResponse()); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - urlConnection.addRequestProperty("D", "e"); - urlConnection.addRequestProperty("D", "f"); - assertEquals("f", urlConnection.getRequestProperty("D")); - assertEquals("f", urlConnection.getRequestProperty("d")); - Map> requestHeaders = urlConnection.getRequestProperties(); - assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("D"))); - assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("d"))); - try { - requestHeaders.put("G", Arrays.asList("h")); - fail("Modified an unmodifiable view."); - } catch (UnsupportedOperationException expected) { - } - try { - requestHeaders.get("D").add("i"); - fail("Modified an unmodifiable view."); - } catch (UnsupportedOperationException expected) { - } - try { - urlConnection.setRequestProperty(null, "j"); - fail(); - } catch (NullPointerException expected) { - } - try { - urlConnection.addRequestProperty(null, "k"); - fail(); - } catch (NullPointerException expected) { - } - urlConnection.setRequestProperty("NullValue", null); // should fail silently! - assertNull(urlConnection.getRequestProperty("NullValue")); - urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently! - assertNull(urlConnection.getRequestProperty("AnotherNullValue")); - - urlConnection.getResponseCode(); - RecordedRequest request = server.takeRequest(); - assertContains(request.getHeaders(), "D: e"); - assertContains(request.getHeaders(), "D: f"); - assertContainsNoneMatching(request.getHeaders(), "NullValue.*"); - assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*"); - assertContainsNoneMatching(request.getHeaders(), "G:.*"); - assertContainsNoneMatching(request.getHeaders(), "null:.*"); - - try { - urlConnection.addRequestProperty("N", "o"); - fail("Set header after connect"); - } catch (IllegalStateException expected) { - } - try { - urlConnection.setRequestProperty("P", "q"); - fail("Set header after connect"); - } catch (IllegalStateException expected) { - } - try { - urlConnection.getRequestProperties(); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void getRequestPropertyReturnsLastValue() throws Exception { - server.play(); - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - urlConnection.addRequestProperty("A", "value1"); - urlConnection.addRequestProperty("A", "value2"); - assertEquals("value2", urlConnection.getRequestProperty("A")); - } - - @Test public void responseHeaders() throws IOException, InterruptedException { - server.enqueue(new MockResponse() - .setStatus("HTTP/1.0 200 Fantastic") - .addHeader("A: c") - .addHeader("B: d") - .addHeader("A: e") - .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8)); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - assertEquals(200, urlConnection.getResponseCode()); - assertEquals("Fantastic", urlConnection.getResponseMessage()); - assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null)); - Map> responseHeaders = urlConnection.getHeaderFields(); - assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null)); - assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("A"))); - assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("a"))); - try { - responseHeaders.put("N", Arrays.asList("o")); - fail("Modified an unmodifiable view."); - } catch (UnsupportedOperationException expected) { - } - try { - responseHeaders.get("A").add("f"); - fail("Modified an unmodifiable view."); - } catch (UnsupportedOperationException expected) { - } - assertEquals("A", urlConnection.getHeaderFieldKey(0)); - assertEquals("c", urlConnection.getHeaderField(0)); - assertEquals("B", urlConnection.getHeaderFieldKey(1)); - assertEquals("d", urlConnection.getHeaderField(1)); - assertEquals("A", urlConnection.getHeaderFieldKey(2)); - assertEquals("e", urlConnection.getHeaderField(2)); - } - - @Test public void serverSendsInvalidResponseHeaders() throws Exception { - server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK")); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - try { - urlConnection.getResponseCode(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void serverSendsInvalidCodeTooLarge() throws Exception { - server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK")); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - try { - urlConnection.getResponseCode(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void serverSendsInvalidCodeNotANumber() throws Exception { - server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK")); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - try { - urlConnection.getResponseCode(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void serverSendsUnnecessaryWhitespace() throws Exception { - server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK")); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - try { - urlConnection.getResponseCode(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void connectRetriesUntilConnectedOrFailed() throws Exception { - server.play(); - URL url = server.getUrl("/foo"); - server.shutdown(); - - HttpURLConnection connection = client.open(url); - try { - connection.connect(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void requestBodySurvivesRetriesWithFixedLength() throws Exception { - testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH); - } - - @Test public void requestBodySurvivesRetriesWithChunkedStreaming() throws Exception { - testRequestBodySurvivesRetries(TransferKind.CHUNKED); - } - - @Test public void requestBodySurvivesRetriesWithBufferedBody() throws Exception { - testRequestBodySurvivesRetries(TransferKind.END_OF_STREAM); - } - - private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception { - server.enqueue(new MockResponse().setBody("abc")); - server.play(); - - // Use a misconfigured proxy to guarantee that the request is retried. - server2.play(); - FakeProxySelector proxySelector = new FakeProxySelector(); - proxySelector.proxies.add(server2.toProxyAddress()); - client.setProxySelector(proxySelector); - server2.shutdown(); - - HttpURLConnection connection = client.open(server.getUrl("/def")); - connection.setDoOutput(true); - transferKind.setForRequest(connection, 4); - connection.getOutputStream().write("body".getBytes("UTF-8")); - assertContent("abc", connection); - - assertEquals("body", server.takeRequest().getUtf8Body()); - } - - @Test public void getErrorStreamOnSuccessfulRequest() throws Exception { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertNull(connection.getErrorStream()); - } - - @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception { - server.enqueue(new MockResponse().setResponseCode(404).setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE)); - } - - // Check that if we don't read to the end of a response, the next request on the - // recycled connection doesn't get the unread tail of the first request's response. - // http://code.google.com/p/android/issues/detail?id=2939 - @Test public void bug2939() throws Exception { - MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8); - - server.enqueue(response); - server.enqueue(response); - server.play(); - - assertContent("ABCDE", client.open(server.getUrl("/")), 5); - assertContent("ABCDE", client.open(server.getUrl("/")), 5); - } - - // Check that we recognize a few basic mime types by extension. - // http://code.google.com/p/android/issues/detail?id=10100 - @Test public void bug10100() throws Exception { - assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg")); - assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf")); - } - - @Test public void connectionsArePooled() throws Exception { - MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"); - - server.enqueue(response); - server.enqueue(response); - server.enqueue(response); - server.play(); - - assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo"))); - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux"))); - assertEquals(1, server.takeRequest().getSequenceNumber()); - assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z"))); - assertEquals(2, server.takeRequest().getSequenceNumber()); - } - - @Test public void chunkedConnectionsArePooled() throws Exception { - MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5); - - server.enqueue(response); - server.enqueue(response); - server.enqueue(response); - server.play(); - - assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo"))); - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux"))); - assertEquals(1, server.takeRequest().getSequenceNumber()); - assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z"))); - assertEquals(2, server.takeRequest().getSequenceNumber()); - } - - @Test public void serverClosesSocket() throws Exception { - testServerClosesOutput(DISCONNECT_AT_END); - } - - @Test public void serverShutdownInput() throws Exception { - testServerClosesOutput(SHUTDOWN_INPUT_AT_END); - } - - /** - * Ignored because this test is racy. - * https://github.com/square/okhttp/issues/90 - */ - @Test @Ignore public void serverShutdownOutput() throws Exception { - testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END); - } - - private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception { - server.enqueue(new MockResponse() - .setBody("This connection won't pool properly") - .setSocketPolicy(socketPolicy)); - MockResponse responseAfter = new MockResponse() - .setBody("This comes after a busted connection"); - server.enqueue(responseAfter); - server.enqueue(responseAfter); // Enqueue 2x because the broken connection may be reused. - server.play(); - - HttpURLConnection connection1 = client.open(server.getUrl("/a")); - connection1.setReadTimeout(100); - assertContent("This connection won't pool properly", connection1); - assertEquals(0, server.takeRequest().getSequenceNumber()); - HttpURLConnection connection2 = client.open(server.getUrl("/b")); - connection2.setReadTimeout(100); - assertContent("This comes after a busted connection", connection2); - - // Check that a fresh connection was created, either immediately or after attempting reuse. - RecordedRequest requestAfter = server.takeRequest(); - if (server.getRequestCount() == 3) { - requestAfter = server.takeRequest(); // The failure consumed a response. - } - // sequence number 0 means the HTTP socket connection was not reused - assertEquals(0, requestAfter.getSequenceNumber()); - } - - enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS } - - @Test public void chunkedUpload_byteByByte() throws Exception { - doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE); - } - - @Test public void chunkedUpload_smallBuffers() throws Exception { - doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS); - } - - @Test public void chunkedUpload_largeBuffers() throws Exception { - doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS); - } - - @Test public void fixedLengthUpload_byteByByte() throws Exception { - doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE); - } - - @Test public void fixedLengthUpload_smallBuffers() throws Exception { - doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS); - } - - @Test public void fixedLengthUpload_largeBuffers() throws Exception { - doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS); - } - - private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception { - int n = 512*1024; - server.setBodyLimit(0); - server.enqueue(new MockResponse()); - server.play(); - - HttpURLConnection conn = client.open(server.getUrl("/")); - conn.setDoOutput(true); - conn.setRequestMethod("POST"); - if (uploadKind == TransferKind.CHUNKED) { - conn.setChunkedStreamingMode(-1); - } else { - conn.setFixedLengthStreamingMode(n); - } - OutputStream out = conn.getOutputStream(); - if (writeKind == WriteKind.BYTE_BY_BYTE) { - for (int i = 0; i < n; ++i) { - out.write('x'); - } - } else { - byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64*1024]; - Arrays.fill(buf, (byte) 'x'); - for (int i = 0; i < n; i += buf.length) { - out.write(buf, 0, Math.min(buf.length, n - i)); - } - } - out.close(); - assertEquals(200, conn.getResponseCode()); - RecordedRequest request = server.takeRequest(); - assertEquals(n, request.getBodySize()); - if (uploadKind == TransferKind.CHUNKED) { - assertTrue(request.getChunkSizes().size() > 0); - } else { - assertTrue(request.getChunkSizes().isEmpty()); - } - } - - @Test public void getResponseCodeNoResponseBody() throws Exception { - server.enqueue(new MockResponse() - .addHeader("abc: def")); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection conn = client.open(url); - conn.setDoInput(false); - assertEquals("def", conn.getHeaderField("abc")); - assertEquals(200, conn.getResponseCode()); - try { - conn.getInputStream(); - fail(); - } catch (ProtocolException expected) { - } - } - - @Test public void connectViaHttps() throws Exception { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); - server.play(); - - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(server.getUrl("/foo")); - - assertContent("this response comes via HTTPS", connection); - - RecordedRequest request = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); - } - - @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); - server.enqueue(new MockResponse().setBody("another response via HTTPS")); - server.play(); - - // The pool will only reuse sockets if the SSL socket factories are the same. - SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory(); - RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - - client.setSSLSocketFactory(clientSocketFactory); - client.setHostnameVerifier(hostnameVerifier); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertContent("this response comes via HTTPS", connection); - - connection = client.open(server.getUrl("/")); - assertContent("another response via HTTPS", connection); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals(1, server.takeRequest().getSequenceNumber()); - } - - @Test public void connectViaHttpsReusingConnectionsDifferentFactories() - throws IOException, InterruptedException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); - server.enqueue(new MockResponse().setBody("another response via HTTPS")); - server.play(); - - // install a custom SSL socket factory so the server can be authorized - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection1 = client.open(server.getUrl("/")); - assertContent("this response comes via HTTPS", connection1); - - client.setSSLSocketFactory(null); - HttpURLConnection connection2 = client.open(server.getUrl("/")); - try { - readAscii(connection2.getInputStream(), Integer.MAX_VALUE); - fail("without an SSL socket factory, the connection should fail"); - } catch (SSLException expected) { - } - } - - @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); - server.enqueue(new MockResponse().setBody("this response comes via SSL")); - server.play(); - - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(server.getUrl("/foo")); - - assertContent("this response comes via SSL", connection); - - RecordedRequest request = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); - } - - /** - * Verify that we don't retry connections on certificate verification errors. - * - * http://code.google.com/p/android/issues/detail?id=13178 - */ - @Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse()); // unused - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/foo")); - try { - connection.getInputStream(); - fail(); - } catch (SSLHandshakeException expected) { - assertTrue(expected.getCause() instanceof CertificateException); - } - assertEquals(0, server.getRequestCount()); - } - - @Test public void connectViaProxyUsingProxyArg() throws Exception { - testConnectViaProxy(ProxyConfig.CREATE_ARG); - } - - @Test public void connectViaProxyUsingProxySystemProperty() throws Exception { - testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY); - } - - @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception { - testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); - } - - private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception { - MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy"); - server.enqueue(mockResponse); - server.play(); - - URL url = new URL("http://android.com/foo"); - HttpURLConnection connection = proxyConfig.connect(server, client, url); - assertContent("this response comes via a proxy", connection); - - RecordedRequest request = server.takeRequest(); - assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine()); - assertContains(request.getHeaders(), "Host: android.com"); - } - - @Test public void contentDisagreesWithContentLengthHeader() throws IOException { - server.enqueue(new MockResponse() - .setBody("abc\r\nYOU SHOULD NOT SEE THIS") - .clearHeaders() - .addHeader("Content-Length: 3")); - server.play(); - - assertContent("abc", client.open(server.getUrl("/"))); - } - - @Test public void contentDisagreesWithChunkedHeader() throws IOException { - MockResponse mockResponse = new MockResponse(); - mockResponse.setChunkedBody("abc", 3); - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - bytesOut.write(mockResponse.getBody()); - bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes("UTF-8")); - mockResponse.setBody(bytesOut.toByteArray()); - mockResponse.clearHeaders(); - mockResponse.addHeader("Transfer-encoding: chunked"); - - server.enqueue(mockResponse); - server.play(); - - assertContent("abc", client.open(server.getUrl("/"))); - } - - @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception { - testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY); - } - - @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception { - // https should not use http proxy - testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); - } - - private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); - server.play(); - - URL url = server.getUrl("/foo"); - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = proxyConfig.connect(server, client, url); - - assertContent("this response comes via HTTPS", connection); - - RecordedRequest request = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); - } - - @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception { - testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG); - } - - /** - * We weren't honoring all of the appropriate proxy system properties when - * connecting via HTTPS. http://b/3097518 - */ - @Test public void connectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception { - testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY); - } - - @Test public void connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception { - testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY); - } - - /** - * We were verifying the wrong hostname when connecting to an HTTPS site - * through a proxy. http://b/3097277 - */ - private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception { - RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - - server.useHttps(sslContext.getSocketFactory(), true); - server.enqueue(new MockResponse() - .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) - .clearHeaders()); - server.enqueue(new MockResponse().setBody("this response comes via a secure proxy")); - server.play(); - - URL url = new URL("https://android.com/foo"); - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(hostnameVerifier); - HttpURLConnection connection = proxyConfig.connect(server, client, url); - - assertContent("this response comes via a secure proxy", connection); - - RecordedRequest connect = server.takeRequest(); - assertEquals("Connect line failure on proxy", - "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine()); - assertContains(connect.getHeaders(), "Host: android.com"); - - RecordedRequest get = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); - assertContains(get.getHeaders(), "Host: android.com"); - assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); - } - - /** - * Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912 - */ - @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() - throws Exception { - initResponseCache(); - - server.useHttps(sslContext.getSocketFactory(), true); - MockResponse response = new MockResponse() // Key to reproducing b/6754912 - .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) - .setBody("bogus proxy connect response content"); - - // Enqueue a pair of responses for every IP address held by localhost, because the - // route selector will try each in sequence. - // TODO: use the fake Dns implementation instead of a loop - for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) { - server.enqueue(response); // For the first TLS tolerant connection - server.enqueue(response); // For the backwards-compatible SSLv3 retry - } - server.play(); - client.setProxy(server.toProxyAddress()); - - URL url = new URL("https://android.com/foo"); - client.setSSLSocketFactory(sslContext.getSocketFactory()); - HttpURLConnection connection = client.open(url); - - try { - connection.getResponseCode(); - fail(); - } catch (IOException expected) { - // Thrown when the connect causes SSLSocket.startHandshake() to throw - // when it sees the "bogus proxy connect response content" - // instead of a ServerHello handshake message. - } - - RecordedRequest connect = server.takeRequest(); - assertEquals("Connect line failure on proxy", - "CONNECT android.com:443 HTTP/1.1", connect.getRequestLine()); - assertContains(connect.getHeaders(), "Host: android.com"); - } - - private void initResponseCache() throws IOException { - String tmp = System.getProperty("java.io.tmpdir"); - File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); - cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); - client.setResponseCache(cache); - } - - /** - * Test which headers are sent unencrypted to the HTTP proxy. - */ - @Test public void proxyConnectIncludesProxyHeadersOnly() - throws IOException, InterruptedException { - RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - - server.useHttps(sslContext.getSocketFactory(), true); - server.enqueue(new MockResponse() - .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) - .clearHeaders()); - server.enqueue(new MockResponse().setBody("encrypted response from the origin server")); - server.play(); - client.setProxy(server.toProxyAddress()); - - URL url = new URL("https://android.com/foo"); - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(hostnameVerifier); - HttpURLConnection connection = client.open(url); - connection.addRequestProperty("Private", "Secret"); - connection.addRequestProperty("Proxy-Authorization", "bar"); - connection.addRequestProperty("User-Agent", "baz"); - assertContent("encrypted response from the origin server", connection); - - RecordedRequest connect = server.takeRequest(); - assertContainsNoneMatching(connect.getHeaders(), "Private.*"); - assertContains(connect.getHeaders(), "Proxy-Authorization: bar"); - assertContains(connect.getHeaders(), "User-Agent: baz"); - assertContains(connect.getHeaders(), "Host: android.com"); - assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive"); - - RecordedRequest get = server.takeRequest(); - assertContains(get.getHeaders(), "Private: Secret"); - assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); - } - - @Test public void proxyAuthenticateOnConnect() throws Exception { - Authenticator.setDefault(new RecordingAuthenticator()); - server.useHttps(sslContext.getSocketFactory(), true); - server.enqueue(new MockResponse() - .setResponseCode(407) - .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); - server.enqueue(new MockResponse() - .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) - .clearHeaders()); - server.enqueue(new MockResponse().setBody("A")); - server.play(); - client.setProxy(server.toProxyAddress()); - - URL url = new URL("https://android.com/foo"); - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(url); - assertContent("A", connection); - - RecordedRequest connect1 = server.takeRequest(); - assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine()); - assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*"); - - RecordedRequest connect2 = server.takeRequest(); - assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine()); - assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic " - + RecordingAuthenticator.BASE_64_CREDENTIALS); - - RecordedRequest get = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); - assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*"); - } - - // Don't disconnect after building a tunnel with CONNECT - // http://code.google.com/p/android/issues/detail?id=37221 - @Test public void proxyWithConnectionClose() throws IOException { - server.useHttps(sslContext.getSocketFactory(), true); - server.enqueue(new MockResponse() - .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) - .clearHeaders()); - server.enqueue(new MockResponse().setBody("this response comes via a proxy")); - server.play(); - client.setProxy(server.toProxyAddress()); - - URL url = new URL("https://android.com/foo"); - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(url); - connection.setRequestProperty("Connection", "close"); - - assertContent("this response comes via a proxy", connection); - } - - @Test public void proxyWithConnectionReuse() throws IOException { - SSLSocketFactory socketFactory = sslContext.getSocketFactory(); - RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - - server.useHttps(socketFactory, true); - server.enqueue(new MockResponse() - .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) - .clearHeaders()); - server.enqueue(new MockResponse().setBody("response 1")); - server.enqueue(new MockResponse().setBody("response 2")); - server.play(); - client.setProxy(server.toProxyAddress()); - - URL url = new URL("https://android.com/foo"); - client.setSSLSocketFactory(socketFactory); - client.setHostnameVerifier(hostnameVerifier); - assertContent("response 1", client.open(url)); - assertContent("response 2", client.open(url)); - } - - @Test public void disconnectedConnection() throws IOException { - server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals('A', (char) in.read()); - connection.disconnect(); - try { - in.read(); - fail("Expected a connection closed exception"); - } catch (IOException expected) { - } - } - - @Test public void disconnectBeforeConnect() throws IOException { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.disconnect(); - - assertContent("A", connection); - assertEquals(200, connection.getResponseCode()); - } - - @SuppressWarnings("deprecation") - @Test public void defaultRequestProperty() throws Exception { - URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A"); - assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty")); - } - - /** - * Reads {@code count} characters from the stream. If the stream is - * exhausted before {@code count} characters can be read, the remaining - * characters are returned and the stream is closed. - */ - private String readAscii(InputStream in, int count) throws IOException { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < count; i++) { - int value = in.read(); - if (value == -1) { - in.close(); - break; - } - result.append((char) value); - } - return result.toString(); - } - - @Test public void markAndResetWithContentLengthHeader() throws IOException { - testMarkAndReset(TransferKind.FIXED_LENGTH); - } - - @Test public void markAndResetWithChunkedEncoding() throws IOException { - testMarkAndReset(TransferKind.CHUNKED); - } - - @Test public void markAndResetWithNoLengthHeaders() throws IOException { - testMarkAndReset(TransferKind.END_OF_STREAM); - } - - private void testMarkAndReset(TransferKind transferKind) throws IOException { - MockResponse response = new MockResponse(); - transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024); - server.enqueue(response); - server.enqueue(response); - server.play(); - - InputStream in = client.open(server.getUrl("/")).getInputStream(); - assertFalse("This implementation claims to support mark().", in.markSupported()); - in.mark(5); - assertEquals("ABCDE", readAscii(in, 5)); - try { - in.reset(); - fail(); - } catch (IOException expected) { - } - assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE)); - assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/"))); - } - - /** - * We've had a bug where we forget the HTTP response when we see response - * code 401. This causes a new HTTP request to be issued for every call into - * the URLConnection. - */ - @Test public void unauthorizedResponseHandling() throws IOException { - MockResponse response = new MockResponse() - .addHeader("WWW-Authenticate: challenge") - .setResponseCode(401) // UNAUTHORIZED - .setBody("Unauthorized"); - server.enqueue(response); - server.enqueue(response); - server.enqueue(response); - server.play(); - - URL url = server.getUrl("/"); - HttpURLConnection conn = client.open(url); - - assertEquals(401, conn.getResponseCode()); - assertEquals(401, conn.getResponseCode()); - assertEquals(401, conn.getResponseCode()); - assertEquals(1, server.getRequestCount()); - } - - @Test public void nonHexChunkSize() throws IOException { - server.enqueue(new MockResponse() - .setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n") - .clearHeaders() - .addHeader("Transfer-encoding: chunked")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - try { - readAscii(connection.getInputStream(), Integer.MAX_VALUE); - fail(); - } catch (IOException e) { - } - } - - @Test public void missingChunkBody() throws IOException { - server.enqueue(new MockResponse() - .setBody("5") - .clearHeaders() - .addHeader("Transfer-encoding: chunked") - .setSocketPolicy(DISCONNECT_AT_END)); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - try { - readAscii(connection.getInputStream(), Integer.MAX_VALUE); - fail(); - } catch (IOException e) { - } - } - - /** - * This test checks whether connections are gzipped by default. This - * behavior in not required by the API, so a failure of this test does not - * imply a bug in the implementation. - */ - @Test public void gzipEncodingEnabledByDefault() throws IOException, InterruptedException { - server.enqueue(new MockResponse() - .setBody(gzip("ABCABCABC".getBytes("UTF-8"))) - .addHeader("Content-Encoding: gzip")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - assertNull(connection.getContentEncoding()); - - RecordedRequest request = server.takeRequest(); - assertContains(request.getHeaders(), "Accept-Encoding: gzip"); - } - - @Test public void clientConfiguredGzipContentEncoding() throws Exception { - server.enqueue(new MockResponse() - .setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"))) - .addHeader("Content-Encoding: gzip")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Accept-Encoding", "gzip"); - InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream()); - assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE)); - - RecordedRequest request = server.takeRequest(); - assertContains(request.getHeaders(), "Accept-Encoding: gzip"); - } - - @Test public void gzipAndConnectionReuseWithFixedLength() throws Exception { - testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, false); - } - - @Test public void gzipAndConnectionReuseWithChunkedEncoding() throws Exception { - testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, false); - } - - @Test public void gzipAndConnectionReuseWithFixedLengthAndTls() throws Exception { - testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, true); - } - - @Test public void gzipAndConnectionReuseWithChunkedEncodingAndTls() throws Exception { - testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, true); - } - - @Test public void clientConfiguredCustomContentEncoding() throws Exception { - server.enqueue(new MockResponse() - .setBody("ABCDE") - .addHeader("Content-Encoding: custom")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - connection.addRequestProperty("Accept-Encoding", "custom"); - assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest request = server.takeRequest(); - assertContains(request.getHeaders(), "Accept-Encoding: custom"); - } - - /** - * Test a bug where gzip input streams weren't exhausting the input stream, - * which corrupted the request that followed or prevented connection reuse. - * http://code.google.com/p/android/issues/detail?id=7059 - * http://code.google.com/p/android/issues/detail?id=38817 - */ - private void testClientConfiguredGzipContentEncodingAndConnectionReuse( - TransferKind transferKind, boolean tls) throws Exception { - if (tls) { - SSLSocketFactory socketFactory = sslContext.getSocketFactory(); - RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - server.useHttps(socketFactory, false); - client.setSSLSocketFactory(socketFactory); - client.setHostnameVerifier(hostnameVerifier); - } - - MockResponse responseOne = new MockResponse(); - responseOne.addHeader("Content-Encoding: gzip"); - transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5); - server.enqueue(responseOne); - MockResponse responseTwo = new MockResponse(); - transferKind.setBody(responseTwo, "two (identity)", 5); - server.enqueue(responseTwo); - server.play(); - - HttpURLConnection connection1 = client.open(server.getUrl("/")); - connection1.addRequestProperty("Accept-Encoding", "gzip"); - InputStream gunzippedIn = new GZIPInputStream(connection1.getInputStream()); - assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE)); - assertEquals(0, server.takeRequest().getSequenceNumber()); - - HttpURLConnection connection2 = client.open(server.getUrl("/")); - assertEquals("two (identity)", readAscii(connection2.getInputStream(), Integer.MAX_VALUE)); - assertEquals(1, server.takeRequest().getSequenceNumber()); - } - - @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception { - testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED); - } - - @Test public void earlyDisconnectDoesntHarmPoolingWithFixedLengthEncoding() throws Exception { - testEarlyDisconnectDoesntHarmPooling(TransferKind.FIXED_LENGTH); - } - - private void testEarlyDisconnectDoesntHarmPooling(TransferKind transferKind) throws Exception { - MockResponse response1 = new MockResponse(); - transferKind.setBody(response1, "ABCDEFGHIJK", 1024); - server.enqueue(response1); - - MockResponse response2 = new MockResponse(); - transferKind.setBody(response2, "LMNOPQRSTUV", 1024); - server.enqueue(response2); - - server.play(); - - URLConnection connection1 = client.open(server.getUrl("/")); - InputStream in1 = connection1.getInputStream(); - assertEquals("ABCDE", readAscii(in1, 5)); - in1.close(); - - HttpURLConnection connection2 = client.open(server.getUrl("/")); - InputStream in2 = connection2.getInputStream(); - assertEquals("LMNOP", readAscii(in2, 5)); - in2.close(); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection is pooled! - } - - /** - * Obnoxiously test that the chunk sizes transmitted exactly equal the - * requested data+chunk header size. Although setChunkedStreamingMode() - * isn't specific about whether the size applies to the data or the - * complete chunk, the RI interprets it as a complete chunk. - */ - @Test public void setChunkedStreamingMode() throws IOException, InterruptedException { - server.enqueue(new MockResponse()); - server.play(); - - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - urlConnection.setChunkedStreamingMode(8); - urlConnection.setDoOutput(true); - OutputStream outputStream = urlConnection.getOutputStream(); - outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII")); - assertEquals(200, urlConnection.getResponseCode()); - - RecordedRequest request = server.takeRequest(); - assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII")); - assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes()); - } - - @Test public void authenticateWithFixedLengthStreaming() throws Exception { - testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH); - } - - @Test public void authenticateWithChunkedStreaming() throws Exception { - testAuthenticateWithStreamingPost(StreamingMode.CHUNKED); - } - - private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception { - MockResponse pleaseAuthenticate = new MockResponse() - .setResponseCode(401) - .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") - .setBody("Please authenticate."); - server.enqueue(pleaseAuthenticate); - server.play(); - - Authenticator.setDefault(new RecordingAuthenticator()); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setDoOutput(true); - byte[] requestBody = { 'A', 'B', 'C', 'D' }; - if (streamingMode == StreamingMode.FIXED_LENGTH) { - connection.setFixedLengthStreamingMode(requestBody.length); - } else if (streamingMode == StreamingMode.CHUNKED) { - connection.setChunkedStreamingMode(0); - } - OutputStream outputStream = connection.getOutputStream(); - outputStream.write(requestBody); - outputStream.close(); - try { - connection.getInputStream(); - fail(); - } catch (HttpRetryException expected) { - } - - // no authorization header for the request... - RecordedRequest request = server.takeRequest(); - assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); - assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); - } - - @Test public void nonStandardAuthenticationScheme() throws Exception { - List calls = authCallsForHeader("WWW-Authenticate: Foo"); - assertEquals(Collections.emptyList(), calls); - } - - @Test public void nonStandardAuthenticationSchemeWithRealm() throws Exception { - List calls = authCallsForHeader("WWW-Authenticate: Foo realm=\"Bar\""); - assertEquals(1, calls.size()); - String call = calls.get(0); - assertTrue(call, call.contains("scheme=Foo")); - assertTrue(call, call.contains("prompt=Bar")); - } - - // Digest auth is currently unsupported. Test that digest requests should fail reasonably. - // http://code.google.com/p/android/issues/detail?id=11140 - @Test public void digestAuthentication() throws Exception { - List calls = authCallsForHeader("WWW-Authenticate: Digest " - + "realm=\"testrealm@host.com\", qop=\"auth,auth-int\", " - + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " - + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""); - assertEquals(1, calls.size()); - String call = calls.get(0); - assertTrue(call, call.contains("scheme=Digest")); - assertTrue(call, call.contains("prompt=testrealm@host.com")); - } - - @Test public void allAttributesSetInServerAuthenticationCallbacks() throws Exception { - List calls = authCallsForHeader("WWW-Authenticate: Basic realm=\"Bar\""); - assertEquals(1, calls.size()); - URL url = server.getUrl("/"); - String call = calls.get(0); - assertTrue(call, call.contains("host=" + url.getHost())); - assertTrue(call, call.contains("port=" + url.getPort())); - assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0])); - assertTrue(call, call.contains("url=" + url)); - assertTrue(call, call.contains("type=" + Authenticator.RequestorType.SERVER)); - assertTrue(call, call.contains("prompt=Bar")); - assertTrue(call, call.contains("protocol=http")); - assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI. - } - - @Test public void allAttributesSetInProxyAuthenticationCallbacks() throws Exception { - List calls = authCallsForHeader("Proxy-Authenticate: Basic realm=\"Bar\""); - assertEquals(1, calls.size()); - URL url = server.getUrl("/"); - String call = calls.get(0); - assertTrue(call, call.contains("host=" + url.getHost())); - assertTrue(call, call.contains("port=" + url.getPort())); - assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0])); - assertTrue(call, call.contains("url=http://android.com")); - assertTrue(call, call.contains("type=" + Authenticator.RequestorType.PROXY)); - assertTrue(call, call.contains("prompt=Bar")); - assertTrue(call, call.contains("protocol=http")); - assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI. - } - - private List authCallsForHeader(String authHeader) throws IOException { - boolean proxy = authHeader.startsWith("Proxy-"); - int responseCode = proxy ? 407 : 401; - RecordingAuthenticator authenticator = new RecordingAuthenticator(null); - Authenticator.setDefault(authenticator); - MockResponse pleaseAuthenticate = new MockResponse() - .setResponseCode(responseCode) - .addHeader(authHeader) - .setBody("Please authenticate."); - server.enqueue(pleaseAuthenticate); - server.play(); - - HttpURLConnection connection; - if (proxy) { - client.setProxy(server.toProxyAddress()); - connection = client.open(new URL("http://android.com")); - } else { - connection = client.open(server.getUrl("/")); - } - assertEquals(responseCode, connection.getResponseCode()); - return authenticator.calls; - } - - @Test public void setValidRequestMethod() throws Exception { - server.play(); - assertValidRequestMethod("GET"); - assertValidRequestMethod("DELETE"); - assertValidRequestMethod("HEAD"); - assertValidRequestMethod("OPTIONS"); - assertValidRequestMethod("POST"); - assertValidRequestMethod("PUT"); - assertValidRequestMethod("TRACE"); - } - - private void assertValidRequestMethod(String requestMethod) throws Exception { - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setRequestMethod(requestMethod); - assertEquals(requestMethod, connection.getRequestMethod()); - } - - @Test public void setInvalidRequestMethodLowercase() throws Exception { - server.play(); - assertInvalidRequestMethod("get"); - } - - @Test public void setInvalidRequestMethodConnect() throws Exception { - server.play(); - assertInvalidRequestMethod("CONNECT"); - } - - private void assertInvalidRequestMethod(String requestMethod) throws Exception { - HttpURLConnection connection = client.open(server.getUrl("/")); - try { - connection.setRequestMethod(requestMethod); - fail(); - } catch (ProtocolException expected) { - } - } - - @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception { - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - try { - connection.setFixedLengthStreamingMode(-2); - fail(); - } catch (IllegalArgumentException expected) { - } - } - - @Test public void canSetNegativeChunkedStreamingMode() throws Exception { - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setChunkedStreamingMode(-2); - } - - @Test public void cannotSetFixedLengthStreamingModeAfterConnect() throws Exception { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - try { - connection.setFixedLengthStreamingMode(1); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - try { - connection.setChunkedStreamingMode(1); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception { - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setChunkedStreamingMode(1); - try { - connection.setFixedLengthStreamingMode(1); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception { - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setFixedLengthStreamingMode(1); - try { - connection.setChunkedStreamingMode(1); - fail(); - } catch (IllegalStateException expected) { - } - } - - @Test public void secureFixedLengthStreaming() throws Exception { - testSecureStreamingPost(StreamingMode.FIXED_LENGTH); - } - - @Test public void secureChunkedStreaming() throws Exception { - testSecureStreamingPost(StreamingMode.CHUNKED); - } - - /** - * Users have reported problems using HTTPS with streaming request bodies. - * http://code.google.com/p/android/issues/detail?id=12860 - */ - private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("Success!")); - server.play(); - - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setDoOutput(true); - byte[] requestBody = { 'A', 'B', 'C', 'D' }; - if (streamingMode == StreamingMode.FIXED_LENGTH) { - connection.setFixedLengthStreamingMode(requestBody.length); - } else if (streamingMode == StreamingMode.CHUNKED) { - connection.setChunkedStreamingMode(0); - } - OutputStream outputStream = connection.getOutputStream(); - outputStream.write(requestBody); - outputStream.close(); - assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest request = server.takeRequest(); - assertEquals("POST / HTTP/1.1", request.getRequestLine()); - if (streamingMode == StreamingMode.FIXED_LENGTH) { - assertEquals(Collections.emptyList(), request.getChunkSizes()); - } else if (streamingMode == StreamingMode.CHUNKED) { - assertEquals(Arrays.asList(4), request.getChunkSizes()); - } - assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); - } - - enum StreamingMode { - FIXED_LENGTH, CHUNKED - } - - @Test public void authenticateWithPost() throws Exception { - MockResponse pleaseAuthenticate = new MockResponse() - .setResponseCode(401) - .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") - .setBody("Please authenticate."); - // fail auth three times... - server.enqueue(pleaseAuthenticate); - server.enqueue(pleaseAuthenticate); - server.enqueue(pleaseAuthenticate); - // ...then succeed the fourth time - server.enqueue(new MockResponse().setBody("Successful auth!")); - server.play(); - - Authenticator.setDefault(new RecordingAuthenticator()); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setDoOutput(true); - byte[] requestBody = { 'A', 'B', 'C', 'D' }; - OutputStream outputStream = connection.getOutputStream(); - outputStream.write(requestBody); - outputStream.close(); - assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - // no authorization header for the first request... - RecordedRequest request = server.takeRequest(); - assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); - - // ...but the three requests that follow include an authorization header - for (int i = 0; i < 3; i++) { - request = server.takeRequest(); - assertEquals("POST / HTTP/1.1", request.getRequestLine()); - assertContains(request.getHeaders(), "Authorization: Basic " - + RecordingAuthenticator.BASE_64_CREDENTIALS); - assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); - } - } - - @Test public void authenticateWithGet() throws Exception { - MockResponse pleaseAuthenticate = new MockResponse() - .setResponseCode(401) - .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") - .setBody("Please authenticate."); - // fail auth three times... - server.enqueue(pleaseAuthenticate); - server.enqueue(pleaseAuthenticate); - server.enqueue(pleaseAuthenticate); - // ...then succeed the fourth time - server.enqueue(new MockResponse().setBody("Successful auth!")); - server.play(); - - Authenticator.setDefault(new RecordingAuthenticator()); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - // no authorization header for the first request... - RecordedRequest request = server.takeRequest(); - assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); - - // ...but the three requests that follow requests include an authorization header - for (int i = 0; i < 3; i++) { - request = server.takeRequest(); - assertEquals("GET / HTTP/1.1", request.getRequestLine()); - assertContains(request.getHeaders(), "Authorization: Basic " - + RecordingAuthenticator.BASE_64_CREDENTIALS); - } - } - - @Test public void redirectedWithChunkedEncoding() throws Exception { - testRedirected(TransferKind.CHUNKED, true); - } - - @Test public void redirectedWithContentLengthHeader() throws Exception { - testRedirected(TransferKind.FIXED_LENGTH, true); - } - - @Test public void redirectedWithNoLengthHeaders() throws Exception { - testRedirected(TransferKind.END_OF_STREAM, false); - } - - private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception { - MockResponse response = new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: /foo"); - transferKind.setBody(response, "This page has moved!", 10); - server.enqueue(response); - server.enqueue(new MockResponse().setBody("This is the new location!")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("This is the new location!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest first = server.takeRequest(); - assertEquals("GET / HTTP/1.1", first.getRequestLine()); - RecordedRequest retry = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); - if (reuse) { - assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); - } - } - - @Test public void redirectedOnHttps() throws IOException, InterruptedException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: /foo") - .setBody("This page has moved!")); - server.enqueue(new MockResponse().setBody("This is the new location!")); - server.play(); - - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("This is the new location!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest first = server.takeRequest(); - assertEquals("GET / HTTP/1.1", first.getRequestLine()); - RecordedRequest retry = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); - assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); - } - - @Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException { - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: http://anyhost/foo") - .setBody("This page has moved!")); - server.play(); - - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(new RecordingHostnameVerifier()); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("This page has moved!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - } - - @Test public void notRedirectedFromHttpToHttps() throws IOException, InterruptedException { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: https://anyhost/foo") - .setBody("This page has moved!")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("This page has moved!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - } - - @Test public void redirectToAnotherOriginServer() throws Exception { - MockWebServer server2 = new MockWebServer(); - server2.enqueue(new MockResponse().setBody("This is the 2nd server!")); - server2.play(); - - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: " + server2.getUrl("/").toString()) - .setBody("This page has moved!")); - server.enqueue(new MockResponse().setBody("This is the first server again!")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("This is the 2nd server!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - assertEquals(server2.getUrl("/"), connection.getURL()); - - // make sure the first server was careful to recycle the connection - assertEquals("This is the first server again!", - readAscii(client.open(server.getUrl("/")).getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest first = server.takeRequest(); - assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort()); - RecordedRequest second = server2.takeRequest(); - assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort()); - RecordedRequest third = server.takeRequest(); - assertEquals("Expected connection reuse", 1, third.getSequenceNumber()); - - server2.shutdown(); - } - - @Test public void response300MultipleChoiceWithPost() throws Exception { - // Chrome doesn't follow the redirect, but Firefox and the RI both do - testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE); - } - - @Test public void response301MovedPermanentlyWithPost() throws Exception { - testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM); - } - - @Test public void response302MovedTemporarilyWithPost() throws Exception { - testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP); - } - - @Test public void response303SeeOtherWithPost() throws Exception { - testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER); - } - - private void testResponseRedirectedWithPost(int redirectCode) throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(redirectCode) - .addHeader("Location: /page2") - .setBody("This page has moved!")); - server.enqueue(new MockResponse().setBody("Page 2")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/page1")); - connection.setDoOutput(true); - byte[] requestBody = { 'A', 'B', 'C', 'D' }; - OutputStream outputStream = connection.getOutputStream(); - outputStream.write(requestBody); - outputStream.close(); - assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - assertTrue(connection.getDoOutput()); - - RecordedRequest page1 = server.takeRequest(); - assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine()); - assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody())); - - RecordedRequest page2 = server.takeRequest(); - assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine()); - } - - @Test public void response305UseProxy() throws Exception { - server.play(); - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_USE_PROXY) - .addHeader("Location: " + server.getUrl("/")) - .setBody("This page has moved!")); - server.enqueue(new MockResponse().setBody("Proxy Response")); - - HttpURLConnection connection = client.open(server.getUrl("/foo")); - // Fails on the RI, which gets "Proxy Response" - assertEquals("This page has moved!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest page1 = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", page1.getRequestLine()); - assertEquals(1, server.getRequestCount()); - } - - @Test public void follow20Redirects() throws Exception { - for (int i = 0; i < 20; i++) { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: /" + (i + 1)) - .setBody("Redirecting to /" + (i + 1))); - } - server.enqueue(new MockResponse().setBody("Success!")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/0")); - assertContent("Success!", connection); - assertEquals(server.getUrl("/20"), connection.getURL()); - } - - @Test public void doesNotFollow21Redirects() throws Exception { - for (int i = 0; i < 21; i++) { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: /" + (i + 1)) - .setBody("Redirecting to /" + (i + 1))); - } - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/0")); - try { - connection.getInputStream(); - fail(); - } catch (ProtocolException expected) { - assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, connection.getResponseCode()); - assertEquals("Too many redirects: 21", expected.getMessage()); - assertContent("Redirecting to /21", connection); - assertEquals(server.getUrl("/20"), connection.getURL()); - } - } - - @Test public void httpsWithCustomTrustManager() throws Exception { - RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); - RecordingTrustManager trustManager = new RecordingTrustManager(); - SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom()); - - client.setHostnameVerifier(hostnameVerifier); - client.setSSLSocketFactory(sc.getSocketFactory()); - server.useHttps(sslContext.getSocketFactory(), false); - server.enqueue(new MockResponse().setBody("ABC")); - server.enqueue(new MockResponse().setBody("DEF")); - server.enqueue(new MockResponse().setBody("GHI")); - server.play(); - - URL url = server.getUrl("/"); - assertContent("ABC", client.open(url)); - assertContent("DEF", client.open(url)); - assertContent("GHI", client.open(url)); - - assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls); - assertEquals(Arrays.asList("checkServerTrusted [CN=" + hostName + " 1]"), - trustManager.calls); - } - - @Test public void readTimeouts() throws IOException { + private MockWebServer server = new MockWebServer(); + private MockWebServer server2 = new MockWebServer(); + + private final OkHttpClient client = new OkHttpClient(); + private HttpResponseCache cache; + private String hostName; + + private static final SSLContext sslContext; + static { + try { + sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + @Before public void setUp() throws Exception { + hostName = server.getHostName(); + } + + @After public void tearDown() throws Exception { + System.clearProperty("proxyHost"); + System.clearProperty("proxyPort"); + System.clearProperty("http.proxyHost"); + System.clearProperty("http.proxyPort"); + System.clearProperty("https.proxyHost"); + System.clearProperty("https.proxyPort"); + server.shutdown(); + server2.shutdown(); + if (cache != null) { + cache.getCache().delete(); + } + } + + @Test public void requestHeaders() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + urlConnection.addRequestProperty("D", "e"); + urlConnection.addRequestProperty("D", "f"); + assertEquals("f", urlConnection.getRequestProperty("D")); + assertEquals("f", urlConnection.getRequestProperty("d")); + Map> requestHeaders = urlConnection.getRequestProperties(); + assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("D"))); + assertEquals(newSet("e", "f"), new HashSet(requestHeaders.get("d"))); + try { + requestHeaders.put("G", Arrays.asList("h")); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + try { + requestHeaders.get("D").add("i"); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + try { + urlConnection.setRequestProperty(null, "j"); + fail(); + } catch (NullPointerException expected) { + } + try { + urlConnection.addRequestProperty(null, "k"); + fail(); + } catch (NullPointerException expected) { + } + urlConnection.setRequestProperty("NullValue", null); // should fail silently! + assertNull(urlConnection.getRequestProperty("NullValue")); + urlConnection.addRequestProperty("AnotherNullValue", null); // should fail silently! + assertNull(urlConnection.getRequestProperty("AnotherNullValue")); + + urlConnection.getResponseCode(); + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "D: e"); + assertContains(request.getHeaders(), "D: f"); + assertContainsNoneMatching(request.getHeaders(), "NullValue.*"); + assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*"); + assertContainsNoneMatching(request.getHeaders(), "G:.*"); + assertContainsNoneMatching(request.getHeaders(), "null:.*"); + + try { + urlConnection.addRequestProperty("N", "o"); + fail("Set header after connect"); + } catch (IllegalStateException expected) { + } + try { + urlConnection.setRequestProperty("P", "q"); + fail("Set header after connect"); + } catch (IllegalStateException expected) { + } + try { + urlConnection.getRequestProperties(); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void getRequestPropertyReturnsLastValue() throws Exception { + server.play(); + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + urlConnection.addRequestProperty("A", "value1"); + urlConnection.addRequestProperty("A", "value2"); + assertEquals("value2", urlConnection.getRequestProperty("A")); + } + + @Test public void responseHeaders() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setStatus("HTTP/1.0 200 Fantastic") + .addHeader("A: c") + .addHeader("B: d") + .addHeader("A: e") + .setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8)); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + assertEquals(200, urlConnection.getResponseCode()); + assertEquals("Fantastic", urlConnection.getResponseMessage()); + assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null)); + Map> responseHeaders = urlConnection.getHeaderFields(); + assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null)); + assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("A"))); + assertEquals(newSet("c", "e"), new HashSet(responseHeaders.get("a"))); + try { + responseHeaders.put("N", Arrays.asList("o")); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + try { + responseHeaders.get("A").add("f"); + fail("Modified an unmodifiable view."); + } catch (UnsupportedOperationException expected) { + } + assertEquals("A", urlConnection.getHeaderFieldKey(0)); + assertEquals("c", urlConnection.getHeaderField(0)); + assertEquals("B", urlConnection.getHeaderFieldKey(1)); + assertEquals("d", urlConnection.getHeaderField(1)); + assertEquals("A", urlConnection.getHeaderFieldKey(2)); + assertEquals("e", urlConnection.getHeaderField(2)); + } + + @Test public void serverSendsInvalidResponseHeaders() throws Exception { + server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK")); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + try { + urlConnection.getResponseCode(); + fail(); + } catch (IOException expected) { + } + } + + @Test public void serverSendsInvalidCodeTooLarge() throws Exception { + server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK")); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + try { + urlConnection.getResponseCode(); + fail(); + } catch (IOException expected) { + } + } + + @Test public void serverSendsInvalidCodeNotANumber() throws Exception { + server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK")); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + try { + urlConnection.getResponseCode(); + fail(); + } catch (IOException expected) { + } + } + + @Test public void serverSendsUnnecessaryWhitespace() throws Exception { + server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK")); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + try { + urlConnection.getResponseCode(); + fail(); + } catch (IOException expected) { + } + } + + @Test public void connectRetriesUntilConnectedOrFailed() throws Exception { + server.play(); + URL url = server.getUrl("/foo"); + server.shutdown(); + + HttpURLConnection connection = client.open(url); + try { + connection.connect(); + fail(); + } catch (IOException expected) { + } + } + + @Test public void requestBodySurvivesRetriesWithFixedLength() throws Exception { + testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH); + } + + @Test public void requestBodySurvivesRetriesWithChunkedStreaming() throws Exception { + testRequestBodySurvivesRetries(TransferKind.CHUNKED); + } + + @Test public void requestBodySurvivesRetriesWithBufferedBody() throws Exception { + testRequestBodySurvivesRetries(TransferKind.END_OF_STREAM); + } + + private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception { + server.enqueue(new MockResponse().setBody("abc")); + server.play(); + + // Use a misconfigured proxy to guarantee that the request is retried. + server2.play(); + FakeProxySelector proxySelector = new FakeProxySelector(); + proxySelector.proxies.add(server2.toProxyAddress()); + client.setProxySelector(proxySelector); + server2.shutdown(); + + HttpURLConnection connection = client.open(server.getUrl("/def")); + connection.setDoOutput(true); + transferKind.setForRequest(connection, 4); + connection.getOutputStream().write("body".getBytes("UTF-8")); + assertContent("abc", connection); + + assertEquals("body", server.takeRequest().getUtf8Body()); + } + + @Test public void getErrorStreamOnSuccessfulRequest() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertNull(connection.getErrorStream()); + } + + @Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception { + server.enqueue(new MockResponse().setResponseCode(404).setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE)); + } + + // Check that if we don't read to the end of a response, the next request on the + // recycled connection doesn't get the unread tail of the first request's response. + // http://code.google.com/p/android/issues/detail?id=2939 + @Test public void bug2939() throws Exception { + MockResponse response = new MockResponse().setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8); + + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertContent("ABCDE", client.open(server.getUrl("/")), 5); + assertContent("ABCDE", client.open(server.getUrl("/")), 5); + } + + // Check that we recognize a few basic mime types by extension. + // http://code.google.com/p/android/issues/detail?id=10100 + @Test public void bug10100() throws Exception { + assertEquals("image/jpeg", URLConnection.guessContentTypeFromName("someFile.jpg")); + assertEquals("application/pdf", URLConnection.guessContentTypeFromName("stuff.pdf")); + } + + @Test public void connectionsArePooled() throws Exception { + MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"); + + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo"))); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux"))); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z"))); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + @Test public void chunkedConnectionsArePooled() throws Exception { + MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5); + + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo"))); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/bar?baz=quux"))); + assertEquals(1, server.takeRequest().getSequenceNumber()); + assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/z"))); + assertEquals(2, server.takeRequest().getSequenceNumber()); + } + + @Test public void serverClosesSocket() throws Exception { + testServerClosesOutput(DISCONNECT_AT_END); + } + + @Test public void serverShutdownInput() throws Exception { + testServerClosesOutput(SHUTDOWN_INPUT_AT_END); + } + + /** + * Ignored because this test is racy. + * https://github.com/square/okhttp/issues/90 + */ + @Test @Ignore public void serverShutdownOutput() throws Exception { + testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END); + } + + private void testServerClosesOutput(SocketPolicy socketPolicy) throws Exception { + server.enqueue(new MockResponse().setBody("This connection won't pool properly") + .setSocketPolicy(socketPolicy)); + MockResponse responseAfter = new MockResponse().setBody("This comes after a busted connection"); + server.enqueue(responseAfter); + server.enqueue(responseAfter); // Enqueue 2x because the broken connection may be reused. + server.play(); + + HttpURLConnection connection1 = client.open(server.getUrl("/a")); + connection1.setReadTimeout(100); + assertContent("This connection won't pool properly", connection1); + assertEquals(0, server.takeRequest().getSequenceNumber()); + HttpURLConnection connection2 = client.open(server.getUrl("/b")); + connection2.setReadTimeout(100); + assertContent("This comes after a busted connection", connection2); + + // Check that a fresh connection was created, either immediately or after attempting reuse. + RecordedRequest requestAfter = server.takeRequest(); + if (server.getRequestCount() == 3) { + requestAfter = server.takeRequest(); // The failure consumed a response. + } + // sequence number 0 means the HTTP socket connection was not reused + assertEquals(0, requestAfter.getSequenceNumber()); + } + + enum WriteKind {BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS} + + @Test public void chunkedUpload_byteByByte() throws Exception { + doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE); + } + + @Test public void chunkedUpload_smallBuffers() throws Exception { + doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS); + } + + @Test public void chunkedUpload_largeBuffers() throws Exception { + doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS); + } + + @Test public void fixedLengthUpload_byteByByte() throws Exception { + doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE); + } + + @Test public void fixedLengthUpload_smallBuffers() throws Exception { + doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS); + } + + @Test public void fixedLengthUpload_largeBuffers() throws Exception { + doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS); + } + + private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception { + int n = 512 * 1024; + server.setBodyLimit(0); + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection conn = client.open(server.getUrl("/")); + conn.setDoOutput(true); + conn.setRequestMethod("POST"); + if (uploadKind == TransferKind.CHUNKED) { + conn.setChunkedStreamingMode(-1); + } else { + conn.setFixedLengthStreamingMode(n); + } + OutputStream out = conn.getOutputStream(); + if (writeKind == WriteKind.BYTE_BY_BYTE) { + for (int i = 0; i < n; ++i) { + out.write('x'); + } + } else { + byte[] buf = new byte[writeKind == WriteKind.SMALL_BUFFERS ? 256 : 64 * 1024]; + Arrays.fill(buf, (byte) 'x'); + for (int i = 0; i < n; i += buf.length) { + out.write(buf, 0, Math.min(buf.length, n - i)); + } + } + out.close(); + assertEquals(200, conn.getResponseCode()); + RecordedRequest request = server.takeRequest(); + assertEquals(n, request.getBodySize()); + if (uploadKind == TransferKind.CHUNKED) { + assertTrue(request.getChunkSizes().size() > 0); + } else { + assertTrue(request.getChunkSizes().isEmpty()); + } + } + + @Test public void getResponseCodeNoResponseBody() throws Exception { + server.enqueue(new MockResponse().addHeader("abc: def")); + server.play(); + + URL url = server.getUrl("/"); + HttpURLConnection conn = client.open(url); + conn.setDoInput(false); + assertEquals("def", conn.getHeaderField("abc")); + assertEquals(200, conn.getResponseCode()); + try { + conn.getInputStream(); + fail(); + } catch (ProtocolException expected) { + } + } + + @Test public void connectViaHttps() throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); + server.play(); + + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(server.getUrl("/foo")); + + assertContent("this response comes via HTTPS", connection); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); + } + + @Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); + server.enqueue(new MockResponse().setBody("another response via HTTPS")); + server.play(); + + // The pool will only reuse sockets if the SSL socket factories are the same. + SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory(); + RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); + + client.setSSLSocketFactory(clientSocketFactory); + client.setHostnameVerifier(hostnameVerifier); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertContent("this response comes via HTTPS", connection); + + connection = client.open(server.getUrl("/")); + assertContent("another response via HTTPS", connection); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(1, server.takeRequest().getSequenceNumber()); + } + + @Test public void connectViaHttpsReusingConnectionsDifferentFactories() + throws IOException, InterruptedException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); + server.enqueue(new MockResponse().setBody("another response via HTTPS")); + server.play(); + + // install a custom SSL socket factory so the server can be authorized + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection1 = client.open(server.getUrl("/")); + assertContent("this response comes via HTTPS", connection1); + + client.setSSLSocketFactory(null); + HttpURLConnection connection2 = client.open(server.getUrl("/")); + try { + readAscii(connection2.getInputStream(), Integer.MAX_VALUE); + fail("without an SSL socket factory, the connection should fail"); + } catch (SSLException expected) { + } + } + + @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE)); + server.enqueue(new MockResponse().setBody("this response comes via SSL")); + server.play(); + + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(server.getUrl("/foo")); + + assertContent("this response comes via SSL", connection); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); + } + + /** + * Verify that we don't retry connections on certificate verification errors. + * + * http://code.google.com/p/android/issues/detail?id=13178 + */ + @Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse()); // unused + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/foo")); + try { + connection.getInputStream(); + fail(); + } catch (SSLHandshakeException expected) { + assertTrue(expected.getCause() instanceof CertificateException); + } + assertEquals(0, server.getRequestCount()); + } + + @Test public void connectViaProxyUsingProxyArg() throws Exception { + testConnectViaProxy(ProxyConfig.CREATE_ARG); + } + + @Test public void connectViaProxyUsingProxySystemProperty() throws Exception { + testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY); + } + + @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception { + testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); + } + + private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception { + MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy"); + server.enqueue(mockResponse); + server.play(); + + URL url = new URL("http://android.com/foo"); + HttpURLConnection connection = proxyConfig.connect(server, client, url); + assertContent("this response comes via a proxy", connection); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), "Host: android.com"); + } + + @Test public void contentDisagreesWithContentLengthHeader() throws IOException { + server.enqueue(new MockResponse().setBody("abc\r\nYOU SHOULD NOT SEE THIS") + .clearHeaders() + .addHeader("Content-Length: 3")); + server.play(); + + assertContent("abc", client.open(server.getUrl("/"))); + } + + @Test public void contentDisagreesWithChunkedHeader() throws IOException { + MockResponse mockResponse = new MockResponse(); + mockResponse.setChunkedBody("abc", 3); + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + bytesOut.write(mockResponse.getBody()); + bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes("UTF-8")); + mockResponse.setBody(bytesOut.toByteArray()); + mockResponse.clearHeaders(); + mockResponse.addHeader("Transfer-encoding: chunked"); + + server.enqueue(mockResponse); + server.play(); + + assertContent("abc", client.open(server.getUrl("/"))); + } + + @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception { + testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY); + } + + @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception { + // https should not use http proxy + testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY); + } + + private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("this response comes via HTTPS")); + server.play(); + + URL url = server.getUrl("/foo"); + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = proxyConfig.connect(server, client, url); + + assertContent("this response comes via HTTPS", connection); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); + } + + @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception { + testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG); + } + + /** + * We weren't honoring all of the appropriate proxy system properties when + * connecting via HTTPS. http://b/3097518 + */ + @Test public void connectViaHttpProxyToHttpsUsingProxySystemProperty() throws Exception { + testConnectViaHttpProxyToHttps(ProxyConfig.PROXY_SYSTEM_PROPERTY); + } + + @Test public void connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception { + testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY); + } + + /** + * We were verifying the wrong hostname when connecting to an HTTPS site + * through a proxy. http://b/3097277 + */ + private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception { + RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); + + server.useHttps(sslContext.getSocketFactory(), true); + server.enqueue( + new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); + server.enqueue(new MockResponse().setBody("this response comes via a secure proxy")); + server.play(); + + URL url = new URL("https://android.com/foo"); + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(hostnameVerifier); + HttpURLConnection connection = proxyConfig.connect(server, client, url); + + assertContent("this response comes via a secure proxy", connection); + + RecordedRequest connect = server.takeRequest(); + assertEquals("Connect line failure on proxy", "CONNECT android.com:443 HTTP/1.1", + connect.getRequestLine()); + assertContains(connect.getHeaders(), "Host: android.com"); + + RecordedRequest get = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); + assertContains(get.getHeaders(), "Host: android.com"); + assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); + } + + /** Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912 */ + @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception { + initResponseCache(); + + server.useHttps(sslContext.getSocketFactory(), true); + MockResponse response = new MockResponse() // Key to reproducing b/6754912 + .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END) + .setBody("bogus proxy connect response content"); + + // Enqueue a pair of responses for every IP address held by localhost, because the + // route selector will try each in sequence. + // TODO: use the fake Dns implementation instead of a loop + for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) { + server.enqueue(response); // For the first TLS tolerant connection + server.enqueue(response); // For the backwards-compatible SSLv3 retry + } + server.play(); + client.setProxy(server.toProxyAddress()); + + URL url = new URL("https://android.com/foo"); + client.setSSLSocketFactory(sslContext.getSocketFactory()); + HttpURLConnection connection = client.open(url); + + try { + connection.getResponseCode(); + fail(); + } catch (IOException expected) { + // Thrown when the connect causes SSLSocket.startHandshake() to throw + // when it sees the "bogus proxy connect response content" + // instead of a ServerHello handshake message. + } + + RecordedRequest connect = server.takeRequest(); + assertEquals("Connect line failure on proxy", "CONNECT android.com:443 HTTP/1.1", + connect.getRequestLine()); + assertContains(connect.getHeaders(), "Host: android.com"); + } + + private void initResponseCache() throws IOException { + String tmp = System.getProperty("java.io.tmpdir"); + File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID()); + cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); + client.setResponseCache(cache); + } + + /** Test which headers are sent unencrypted to the HTTP proxy. */ + @Test public void proxyConnectIncludesProxyHeadersOnly() + throws IOException, InterruptedException { + RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); + + server.useHttps(sslContext.getSocketFactory(), true); + server.enqueue( + new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); + server.enqueue(new MockResponse().setBody("encrypted response from the origin server")); + server.play(); + client.setProxy(server.toProxyAddress()); + + URL url = new URL("https://android.com/foo"); + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(hostnameVerifier); + HttpURLConnection connection = client.open(url); + connection.addRequestProperty("Private", "Secret"); + connection.addRequestProperty("Proxy-Authorization", "bar"); + connection.addRequestProperty("User-Agent", "baz"); + assertContent("encrypted response from the origin server", connection); + + RecordedRequest connect = server.takeRequest(); + assertContainsNoneMatching(connect.getHeaders(), "Private.*"); + assertContains(connect.getHeaders(), "Proxy-Authorization: bar"); + assertContains(connect.getHeaders(), "User-Agent: baz"); + assertContains(connect.getHeaders(), "Host: android.com"); + assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive"); + + RecordedRequest get = server.takeRequest(); + assertContains(get.getHeaders(), "Private: Secret"); + assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls); + } + + @Test public void proxyAuthenticateOnConnect() throws Exception { + Authenticator.setDefault(new RecordingAuthenticator()); + server.useHttps(sslContext.getSocketFactory(), true); + server.enqueue(new MockResponse().setResponseCode(407) + .addHeader("Proxy-Authenticate: Basic realm=\"localhost\"")); + server.enqueue( + new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); + server.enqueue(new MockResponse().setBody("A")); + server.play(); + client.setProxy(server.toProxyAddress()); + + URL url = new URL("https://android.com/foo"); + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(url); + assertContent("A", connection); + + RecordedRequest connect1 = server.takeRequest(); + assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine()); + assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*"); + + RecordedRequest connect2 = server.takeRequest(); + assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine()); + assertContains(connect2.getHeaders(), + "Proxy-Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); + + RecordedRequest get = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", get.getRequestLine()); + assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*"); + } + + // Don't disconnect after building a tunnel with CONNECT + // http://code.google.com/p/android/issues/detail?id=37221 + @Test public void proxyWithConnectionClose() throws IOException { + server.useHttps(sslContext.getSocketFactory(), true); + server.enqueue( + new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); + server.enqueue(new MockResponse().setBody("this response comes via a proxy")); + server.play(); + client.setProxy(server.toProxyAddress()); + + URL url = new URL("https://android.com/foo"); + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(url); + connection.setRequestProperty("Connection", "close"); + + assertContent("this response comes via a proxy", connection); + } + + @Test public void proxyWithConnectionReuse() throws IOException { + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); + + server.useHttps(socketFactory, true); + server.enqueue( + new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders()); + server.enqueue(new MockResponse().setBody("response 1")); + server.enqueue(new MockResponse().setBody("response 2")); + server.play(); + client.setProxy(server.toProxyAddress()); + + URL url = new URL("https://android.com/foo"); + client.setSSLSocketFactory(socketFactory); + client.setHostnameVerifier(hostnameVerifier); + assertContent("response 1", client.open(url)); + assertContent("response 2", client.open(url)); + } + + @Test public void disconnectedConnection() throws IOException { + server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals('A', (char) in.read()); + connection.disconnect(); + try { + in.read(); + fail("Expected a connection closed exception"); + } catch (IOException expected) { + } + } + + @Test public void disconnectBeforeConnect() throws IOException { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.disconnect(); + + assertContent("A", connection); + assertEquals(200, connection.getResponseCode()); + } + + @SuppressWarnings("deprecation") @Test public void defaultRequestProperty() throws Exception { + URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A"); + assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty")); + } + + /** + * Reads {@code count} characters from the stream. If the stream is + * exhausted before {@code count} characters can be read, the remaining + * characters are returned and the stream is closed. + */ + private String readAscii(InputStream in, int count) throws IOException { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < count; i++) { + int value = in.read(); + if (value == -1) { + in.close(); + break; + } + result.append((char) value); + } + return result.toString(); + } + + @Test public void markAndResetWithContentLengthHeader() throws IOException { + testMarkAndReset(TransferKind.FIXED_LENGTH); + } + + @Test public void markAndResetWithChunkedEncoding() throws IOException { + testMarkAndReset(TransferKind.CHUNKED); + } + + @Test public void markAndResetWithNoLengthHeaders() throws IOException { + testMarkAndReset(TransferKind.END_OF_STREAM); + } + + private void testMarkAndReset(TransferKind transferKind) throws IOException { + MockResponse response = new MockResponse(); + transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024); + server.enqueue(response); + server.enqueue(response); + server.play(); + + InputStream in = client.open(server.getUrl("/")).getInputStream(); + assertFalse("This implementation claims to support mark().", in.markSupported()); + in.mark(5); + assertEquals("ABCDE", readAscii(in, 5)); + try { + in.reset(); + fail(); + } catch (IOException expected) { + } + assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE)); + assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/"))); + } + + /** + * We've had a bug where we forget the HTTP response when we see response + * code 401. This causes a new HTTP request to be issued for every call into + * the URLConnection. + */ + @Test public void unauthorizedResponseHandling() throws IOException { + MockResponse response = new MockResponse().addHeader("WWW-Authenticate: challenge") + .setResponseCode(401) // UNAUTHORIZED + .setBody("Unauthorized"); + server.enqueue(response); + server.enqueue(response); + server.enqueue(response); + server.play(); + + URL url = server.getUrl("/"); + HttpURLConnection conn = client.open(url); + + assertEquals(401, conn.getResponseCode()); + assertEquals(401, conn.getResponseCode()); + assertEquals(401, conn.getResponseCode()); + assertEquals(1, server.getRequestCount()); + } + + @Test public void nonHexChunkSize() throws IOException { + server.enqueue(new MockResponse().setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n") + .clearHeaders() + .addHeader("Transfer-encoding: chunked")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + try { + readAscii(connection.getInputStream(), Integer.MAX_VALUE); + fail(); + } catch (IOException e) { + } + } + + @Test public void missingChunkBody() throws IOException { + server.enqueue(new MockResponse().setBody("5") + .clearHeaders() + .addHeader("Transfer-encoding: chunked") + .setSocketPolicy(DISCONNECT_AT_END)); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + try { + readAscii(connection.getInputStream(), Integer.MAX_VALUE); + fail(); + } catch (IOException e) { + } + } + + /** + * This test checks whether connections are gzipped by default. This + * behavior in not required by the API, so a failure of this test does not + * imply a bug in the implementation. + */ + @Test public void gzipEncodingEnabledByDefault() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setBody(gzip("ABCABCABC".getBytes("UTF-8"))) + .addHeader("Content-Encoding: gzip")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertNull(connection.getContentEncoding()); + + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Accept-Encoding: gzip"); + } + + @Test public void clientConfiguredGzipContentEncoding() throws Exception { + server.enqueue(new MockResponse().setBody(gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"))) + .addHeader("Content-Encoding: gzip")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Accept-Encoding", "gzip"); + InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream()); + assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE)); + + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Accept-Encoding: gzip"); + } + + @Test public void gzipAndConnectionReuseWithFixedLength() throws Exception { + testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, false); + } + + @Test public void gzipAndConnectionReuseWithChunkedEncoding() throws Exception { + testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, false); + } + + @Test public void gzipAndConnectionReuseWithFixedLengthAndTls() throws Exception { + testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.FIXED_LENGTH, true); + } + + @Test public void gzipAndConnectionReuseWithChunkedEncodingAndTls() throws Exception { + testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind.CHUNKED, true); + } + + @Test public void clientConfiguredCustomContentEncoding() throws Exception { + server.enqueue(new MockResponse().setBody("ABCDE").addHeader("Content-Encoding: custom")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + connection.addRequestProperty("Accept-Encoding", "custom"); + assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Accept-Encoding: custom"); + } + + /** + * Test a bug where gzip input streams weren't exhausting the input stream, + * which corrupted the request that followed or prevented connection reuse. + * http://code.google.com/p/android/issues/detail?id=7059 + * http://code.google.com/p/android/issues/detail?id=38817 + */ + private void testClientConfiguredGzipContentEncodingAndConnectionReuse(TransferKind transferKind, + boolean tls) throws Exception { + if (tls) { + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); + server.useHttps(socketFactory, false); + client.setSSLSocketFactory(socketFactory); + client.setHostnameVerifier(hostnameVerifier); + } + + MockResponse responseOne = new MockResponse(); + responseOne.addHeader("Content-Encoding: gzip"); + transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5); + server.enqueue(responseOne); + MockResponse responseTwo = new MockResponse(); + transferKind.setBody(responseTwo, "two (identity)", 5); + server.enqueue(responseTwo); + server.play(); + + HttpURLConnection connection1 = client.open(server.getUrl("/")); + connection1.addRequestProperty("Accept-Encoding", "gzip"); + InputStream gunzippedIn = new GZIPInputStream(connection1.getInputStream()); + assertEquals("one (gzipped)", readAscii(gunzippedIn, Integer.MAX_VALUE)); + assertEquals(0, server.takeRequest().getSequenceNumber()); + + HttpURLConnection connection2 = client.open(server.getUrl("/")); + assertEquals("two (identity)", readAscii(connection2.getInputStream(), Integer.MAX_VALUE)); + assertEquals(1, server.takeRequest().getSequenceNumber()); + } + + @Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception { + testEarlyDisconnectDoesntHarmPooling(TransferKind.CHUNKED); + } + + @Test public void earlyDisconnectDoesntHarmPoolingWithFixedLengthEncoding() throws Exception { + testEarlyDisconnectDoesntHarmPooling(TransferKind.FIXED_LENGTH); + } + + private void testEarlyDisconnectDoesntHarmPooling(TransferKind transferKind) throws Exception { + MockResponse response1 = new MockResponse(); + transferKind.setBody(response1, "ABCDEFGHIJK", 1024); + server.enqueue(response1); + + MockResponse response2 = new MockResponse(); + transferKind.setBody(response2, "LMNOPQRSTUV", 1024); + server.enqueue(response2); + + server.play(); + + URLConnection connection1 = client.open(server.getUrl("/")); + InputStream in1 = connection1.getInputStream(); + assertEquals("ABCDE", readAscii(in1, 5)); + in1.close(); + + HttpURLConnection connection2 = client.open(server.getUrl("/")); + InputStream in2 = connection2.getInputStream(); + assertEquals("LMNOP", readAscii(in2, 5)); + in2.close(); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection is pooled! + } + + /** + * Obnoxiously test that the chunk sizes transmitted exactly equal the + * requested data+chunk header size. Although setChunkedStreamingMode() + * isn't specific about whether the size applies to the data or the + * complete chunk, the RI interprets it as a complete chunk. + */ + @Test public void setChunkedStreamingMode() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + urlConnection.setChunkedStreamingMode(8); + urlConnection.setDoOutput(true); + OutputStream outputStream = urlConnection.getOutputStream(); + outputStream.write("ABCDEFGHIJKLMNOPQ".getBytes("US-ASCII")); + assertEquals(200, urlConnection.getResponseCode()); + + RecordedRequest request = server.takeRequest(); + assertEquals("ABCDEFGHIJKLMNOPQ", new String(request.getBody(), "US-ASCII")); + assertEquals(Arrays.asList(3, 3, 3, 3, 3, 2), request.getChunkSizes()); + } + + @Test public void authenticateWithFixedLengthStreaming() throws Exception { + testAuthenticateWithStreamingPost(StreamingMode.FIXED_LENGTH); + } + + @Test public void authenticateWithChunkedStreaming() throws Exception { + testAuthenticateWithStreamingPost(StreamingMode.CHUNKED); + } + + private void testAuthenticateWithStreamingPost(StreamingMode streamingMode) throws Exception { + MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401) + .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate."); + server.enqueue(pleaseAuthenticate); + server.play(); + + Authenticator.setDefault(new RecordingAuthenticator()); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + if (streamingMode == StreamingMode.FIXED_LENGTH) { + connection.setFixedLengthStreamingMode(requestBody.length); + } else if (streamingMode == StreamingMode.CHUNKED) { + connection.setChunkedStreamingMode(0); + } + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + try { + connection.getInputStream(); + fail(); + } catch (HttpRetryException expected) { + } + + // no authorization header for the request... + RecordedRequest request = server.takeRequest(); + assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); + assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); + } + + @Test public void nonStandardAuthenticationScheme() throws Exception { + List calls = authCallsForHeader("WWW-Authenticate: Foo"); + assertEquals(Collections.emptyList(), calls); + } + + @Test public void nonStandardAuthenticationSchemeWithRealm() throws Exception { + List calls = authCallsForHeader("WWW-Authenticate: Foo realm=\"Bar\""); + assertEquals(1, calls.size()); + String call = calls.get(0); + assertTrue(call, call.contains("scheme=Foo")); + assertTrue(call, call.contains("prompt=Bar")); + } + + // Digest auth is currently unsupported. Test that digest requests should fail reasonably. + // http://code.google.com/p/android/issues/detail?id=11140 + @Test public void digestAuthentication() throws Exception { + List calls = authCallsForHeader("WWW-Authenticate: Digest " + + "realm=\"testrealm@host.com\", qop=\"auth,auth-int\", " + + "nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\", " + + "opaque=\"5ccc069c403ebaf9f0171e9517f40e41\""); + assertEquals(1, calls.size()); + String call = calls.get(0); + assertTrue(call, call.contains("scheme=Digest")); + assertTrue(call, call.contains("prompt=testrealm@host.com")); + } + + @Test public void allAttributesSetInServerAuthenticationCallbacks() throws Exception { + List calls = authCallsForHeader("WWW-Authenticate: Basic realm=\"Bar\""); + assertEquals(1, calls.size()); + URL url = server.getUrl("/"); + String call = calls.get(0); + assertTrue(call, call.contains("host=" + url.getHost())); + assertTrue(call, call.contains("port=" + url.getPort())); + assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0])); + assertTrue(call, call.contains("url=" + url)); + assertTrue(call, call.contains("type=" + Authenticator.RequestorType.SERVER)); + assertTrue(call, call.contains("prompt=Bar")); + assertTrue(call, call.contains("protocol=http")); + assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI. + } + + @Test public void allAttributesSetInProxyAuthenticationCallbacks() throws Exception { + List calls = authCallsForHeader("Proxy-Authenticate: Basic realm=\"Bar\""); + assertEquals(1, calls.size()); + URL url = server.getUrl("/"); + String call = calls.get(0); + assertTrue(call, call.contains("host=" + url.getHost())); + assertTrue(call, call.contains("port=" + url.getPort())); + assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0])); + assertTrue(call, call.contains("url=http://android.com")); + assertTrue(call, call.contains("type=" + Authenticator.RequestorType.PROXY)); + assertTrue(call, call.contains("prompt=Bar")); + assertTrue(call, call.contains("protocol=http")); + assertTrue(call, call.toLowerCase().contains("scheme=basic")); // lowercase for the RI. + } + + private List authCallsForHeader(String authHeader) throws IOException { + boolean proxy = authHeader.startsWith("Proxy-"); + int responseCode = proxy ? 407 : 401; + RecordingAuthenticator authenticator = new RecordingAuthenticator(null); + Authenticator.setDefault(authenticator); + MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(responseCode) + .addHeader(authHeader) + .setBody("Please authenticate."); + server.enqueue(pleaseAuthenticate); + server.play(); + + HttpURLConnection connection; + if (proxy) { + client.setProxy(server.toProxyAddress()); + connection = client.open(new URL("http://android.com")); + } else { + connection = client.open(server.getUrl("/")); + } + assertEquals(responseCode, connection.getResponseCode()); + return authenticator.calls; + } + + @Test public void setValidRequestMethod() throws Exception { + server.play(); + assertValidRequestMethod("GET"); + assertValidRequestMethod("DELETE"); + assertValidRequestMethod("HEAD"); + assertValidRequestMethod("OPTIONS"); + assertValidRequestMethod("POST"); + assertValidRequestMethod("PUT"); + assertValidRequestMethod("TRACE"); + } + + private void assertValidRequestMethod(String requestMethod) throws Exception { + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setRequestMethod(requestMethod); + assertEquals(requestMethod, connection.getRequestMethod()); + } + + @Test public void setInvalidRequestMethodLowercase() throws Exception { + server.play(); + assertInvalidRequestMethod("get"); + } + + @Test public void setInvalidRequestMethodConnect() throws Exception { + server.play(); + assertInvalidRequestMethod("CONNECT"); + } + + private void assertInvalidRequestMethod(String requestMethod) throws Exception { + HttpURLConnection connection = client.open(server.getUrl("/")); + try { + connection.setRequestMethod(requestMethod); + fail(); + } catch (ProtocolException expected) { + } + } + + @Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception { + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + try { + connection.setFixedLengthStreamingMode(-2); + fail(); + } catch (IllegalArgumentException expected) { + } + } + + @Test public void canSetNegativeChunkedStreamingMode() throws Exception { + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setChunkedStreamingMode(-2); + } + + @Test public void cannotSetFixedLengthStreamingModeAfterConnect() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + try { + connection.setFixedLengthStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + try { + connection.setChunkedStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception { + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setChunkedStreamingMode(1); + try { + connection.setFixedLengthStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception { + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setFixedLengthStreamingMode(1); + try { + connection.setChunkedStreamingMode(1); + fail(); + } catch (IllegalStateException expected) { + } + } + + @Test public void secureFixedLengthStreaming() throws Exception { + testSecureStreamingPost(StreamingMode.FIXED_LENGTH); + } + + @Test public void secureChunkedStreaming() throws Exception { + testSecureStreamingPost(StreamingMode.CHUNKED); + } + + /** + * Users have reported problems using HTTPS with streaming request bodies. + * http://code.google.com/p/android/issues/detail?id=12860 + */ + private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("Success!")); + server.play(); + + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + if (streamingMode == StreamingMode.FIXED_LENGTH) { + connection.setFixedLengthStreamingMode(requestBody.length); + } else if (streamingMode == StreamingMode.CHUNKED) { + connection.setChunkedStreamingMode(0); + } + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + assertEquals("Success!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest request = server.takeRequest(); + assertEquals("POST / HTTP/1.1", request.getRequestLine()); + if (streamingMode == StreamingMode.FIXED_LENGTH) { + assertEquals(Collections.emptyList(), request.getChunkSizes()); + } else if (streamingMode == StreamingMode.CHUNKED) { + assertEquals(Arrays.asList(4), request.getChunkSizes()); + } + assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); + } + + enum StreamingMode { + FIXED_LENGTH, CHUNKED + } + + @Test public void authenticateWithPost() throws Exception { + MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401) + .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate."); + // fail auth three times... + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + // ...then succeed the fourth time + server.enqueue(new MockResponse().setBody("Successful auth!")); + server.play(); + + Authenticator.setDefault(new RecordingAuthenticator()); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + // no authorization header for the first request... + RecordedRequest request = server.takeRequest(); + assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); + + // ...but the three requests that follow include an authorization header + for (int i = 0; i < 3; i++) { + request = server.takeRequest(); + assertEquals("POST / HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), + "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); + assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody())); + } + } + + @Test public void authenticateWithGet() throws Exception { + MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401) + .addHeader("WWW-Authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate."); + // fail auth three times... + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + server.enqueue(pleaseAuthenticate); + // ...then succeed the fourth time + server.enqueue(new MockResponse().setBody("Successful auth!")); + server.play(); + + Authenticator.setDefault(new RecordingAuthenticator()); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + // no authorization header for the first request... + RecordedRequest request = server.takeRequest(); + assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*"); + + // ...but the three requests that follow requests include an authorization header + for (int i = 0; i < 3; i++) { + request = server.takeRequest(); + assertEquals("GET / HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), + "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); + } + } + + @Test public void redirectedWithChunkedEncoding() throws Exception { + testRedirected(TransferKind.CHUNKED, true); + } + + @Test public void redirectedWithContentLengthHeader() throws Exception { + testRedirected(TransferKind.FIXED_LENGTH, true); + } + + @Test public void redirectedWithNoLengthHeaders() throws Exception { + testRedirected(TransferKind.END_OF_STREAM, false); + } + + private void testRedirected(TransferKind transferKind, boolean reuse) throws Exception { + MockResponse response = new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /foo"); + transferKind.setBody(response, "This page has moved!", 10); + server.enqueue(response); + server.enqueue(new MockResponse().setBody("This is the new location!")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("This is the new location!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest first = server.takeRequest(); + assertEquals("GET / HTTP/1.1", first.getRequestLine()); + RecordedRequest retry = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); + if (reuse) { + assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); + } + } + + @Test public void redirectedOnHttps() throws IOException, InterruptedException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /foo") + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("This is the new location!")); + server.play(); + + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("This is the new location!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest first = server.takeRequest(); + assertEquals("GET / HTTP/1.1", first.getRequestLine()); + RecordedRequest retry = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", retry.getRequestLine()); + assertEquals("Expected connection reuse", 1, retry.getSequenceNumber()); + } + + @Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException { + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: http://anyhost/foo") + .setBody("This page has moved!")); + server.play(); + + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(new RecordingHostnameVerifier()); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + } + + @Test public void notRedirectedFromHttpToHttps() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: https://anyhost/foo") + .setBody("This page has moved!")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + } + + @Test public void redirectToAnotherOriginServer() throws Exception { + MockWebServer server2 = new MockWebServer(); + server2.enqueue(new MockResponse().setBody("This is the 2nd server!")); + server2.play(); + + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: " + server2.getUrl("/").toString()) + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("This is the first server again!")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("This is the 2nd server!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertEquals(server2.getUrl("/"), connection.getURL()); + + // make sure the first server was careful to recycle the connection + assertEquals("This is the first server again!", + readAscii(client.open(server.getUrl("/")).getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest first = server.takeRequest(); + assertContains(first.getHeaders(), "Host: " + hostName + ":" + server.getPort()); + RecordedRequest second = server2.takeRequest(); + assertContains(second.getHeaders(), "Host: " + hostName + ":" + server2.getPort()); + RecordedRequest third = server.takeRequest(); + assertEquals("Expected connection reuse", 1, third.getSequenceNumber()); + + server2.shutdown(); + } + + @Test public void response300MultipleChoiceWithPost() throws Exception { + // Chrome doesn't follow the redirect, but Firefox and the RI both do + testResponseRedirectedWithPost(HttpURLConnection.HTTP_MULT_CHOICE); + } + + @Test public void response301MovedPermanentlyWithPost() throws Exception { + testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_PERM); + } + + @Test public void response302MovedTemporarilyWithPost() throws Exception { + testResponseRedirectedWithPost(HttpURLConnection.HTTP_MOVED_TEMP); + } + + @Test public void response303SeeOtherWithPost() throws Exception { + testResponseRedirectedWithPost(HttpURLConnection.HTTP_SEE_OTHER); + } + + private void testResponseRedirectedWithPost(int redirectCode) throws Exception { + server.enqueue(new MockResponse().setResponseCode(redirectCode) + .addHeader("Location: /page2") + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("Page 2")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/page1")); + connection.setDoOutput(true); + byte[] requestBody = { 'A', 'B', 'C', 'D' }; + OutputStream outputStream = connection.getOutputStream(); + outputStream.write(requestBody); + outputStream.close(); + assertEquals("Page 2", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + assertTrue(connection.getDoOutput()); + + RecordedRequest page1 = server.takeRequest(); + assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine()); + assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody())); + + RecordedRequest page2 = server.takeRequest(); + assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine()); + } + + @Test public void response305UseProxy() throws Exception { + server.play(); + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_USE_PROXY) + .addHeader("Location: " + server.getUrl("/")) + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("Proxy Response")); + + HttpURLConnection connection = client.open(server.getUrl("/foo")); + // Fails on the RI, which gets "Proxy Response" + assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest page1 = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", page1.getRequestLine()); + assertEquals(1, server.getRequestCount()); + } + + @Test public void follow20Redirects() throws Exception { + for (int i = 0; i < 20; i++) { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /" + (i + 1)) + .setBody("Redirecting to /" + (i + 1))); + } + server.enqueue(new MockResponse().setBody("Success!")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/0")); + assertContent("Success!", connection); + assertEquals(server.getUrl("/20"), connection.getURL()); + } + + @Test public void doesNotFollow21Redirects() throws Exception { + for (int i = 0; i < 21; i++) { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /" + (i + 1)) + .setBody("Redirecting to /" + (i + 1))); + } + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/0")); + try { + connection.getInputStream(); + fail(); + } catch (ProtocolException expected) { + assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, connection.getResponseCode()); + assertEquals("Too many redirects: 21", expected.getMessage()); + assertContent("Redirecting to /21", connection); + assertEquals(server.getUrl("/20"), connection.getURL()); + } + } + + @Test public void httpsWithCustomTrustManager() throws Exception { + RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier(); + RecordingTrustManager trustManager = new RecordingTrustManager(); + SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom()); + + client.setHostnameVerifier(hostnameVerifier); + client.setSSLSocketFactory(sc.getSocketFactory()); + server.useHttps(sslContext.getSocketFactory(), false); + server.enqueue(new MockResponse().setBody("ABC")); + server.enqueue(new MockResponse().setBody("DEF")); + server.enqueue(new MockResponse().setBody("GHI")); + server.play(); + + URL url = server.getUrl("/"); + assertContent("ABC", client.open(url)); + assertContent("DEF", client.open(url)); + assertContent("GHI", client.open(url)); + + assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls); + assertEquals(Arrays.asList("checkServerTrusted [CN=" + hostName + " 1]"), trustManager.calls); + } + + @Test public void readTimeouts() throws IOException { /* * This relies on the fact that MockWebServer doesn't close the * connection after a response has been sent. This causes the client to * try to read more bytes than are sent, which results in a timeout. */ - MockResponse timeout = new MockResponse() - .setBody("ABC") - .clearHeaders() - .addHeader("Content-Length: 4"); - server.enqueue(timeout); - server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive - server.play(); + MockResponse timeout = + new MockResponse().setBody("ABC").clearHeaders().addHeader("Content-Length: 4"); + server.enqueue(timeout); + server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive + server.play(); - URLConnection urlConnection = client.open(server.getUrl("/")); - urlConnection.setReadTimeout(1000); - InputStream in = urlConnection.getInputStream(); - assertEquals('A', in.read()); - assertEquals('B', in.read()); - assertEquals('C', in.read()); - try { - in.read(); // if Content-Length was accurate, this would return -1 immediately - fail(); - } catch (SocketTimeoutException expected) { - } + URLConnection urlConnection = client.open(server.getUrl("/")); + urlConnection.setReadTimeout(1000); + InputStream in = urlConnection.getInputStream(); + assertEquals('A', in.read()); + assertEquals('B', in.read()); + assertEquals('C', in.read()); + try { + in.read(); // if Content-Length was accurate, this would return -1 immediately + fail(); + } catch (SocketTimeoutException expected) { + } + } + + @Test public void setChunkedEncodingAsRequestProperty() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection urlConnection = client.open(server.getUrl("/")); + urlConnection.setRequestProperty("Transfer-encoding", "chunked"); + urlConnection.setDoOutput(true); + urlConnection.getOutputStream().write("ABC".getBytes("UTF-8")); + assertEquals(200, urlConnection.getResponseCode()); + + RecordedRequest request = server.takeRequest(); + assertEquals("ABC", new String(request.getBody(), "UTF-8")); + } + + @Test public void connectionCloseInRequest() throws IOException, InterruptedException { + server.enqueue(new MockResponse()); // server doesn't honor the connection: close header! + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection a = client.open(server.getUrl("/")); + a.setRequestProperty("Connection", "close"); + assertEquals(200, a.getResponseCode()); + + HttpURLConnection b = client.open(server.getUrl("/")); + assertEquals(200, b.getResponseCode()); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals("When connection: close is used, each request should get its own connection", 0, + server.takeRequest().getSequenceNumber()); + } + + @Test public void connectionCloseInResponse() throws IOException, InterruptedException { + server.enqueue(new MockResponse().addHeader("Connection: close")); + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection a = client.open(server.getUrl("/")); + assertEquals(200, a.getResponseCode()); + + HttpURLConnection b = client.open(server.getUrl("/")); + assertEquals(200, b.getResponseCode()); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals("When connection: close is used, each request should get its own connection", 0, + server.takeRequest().getSequenceNumber()); + } + + @Test public void connectionCloseWithRedirect() throws IOException, InterruptedException { + MockResponse response = new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /foo") + .addHeader("Connection: close"); + server.enqueue(response); + server.enqueue(new MockResponse().setBody("This is the new location!")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("This is the new location!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals("When connection: close is used, each request should get its own connection", 0, + server.takeRequest().getSequenceNumber()); + } + + /** + * Retry redirects if the socket is closed. + * https://code.google.com/p/android/issues/detail?id=41576 + */ + @Test public void sameConnectionRedirectAndReuse() throws Exception { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .setSocketPolicy(SHUTDOWN_INPUT_AT_END) + .addHeader("Location: /foo")); + server.enqueue(new MockResponse().setBody("This is the new page!")); + server.play(); + + assertContent("This is the new page!", client.open(server.getUrl("/"))); + + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(0, server.takeRequest().getSequenceNumber()); + } + + @Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NO_CONTENT) + .setBody("This body is not allowed!")); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + assertEquals("This body is not allowed!", + readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + } + + @Test public void singleByteReadIsSigned() throws IOException { + server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 })); + server.play(); + + URLConnection connection = client.open(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals(254, in.read()); + assertEquals(255, in.read()); + assertEquals(-1, in.read()); + } + + @Test public void flushAfterStreamTransmittedWithChunkedEncoding() throws IOException { + testFlushAfterStreamTransmitted(TransferKind.CHUNKED); + } + + @Test public void flushAfterStreamTransmittedWithFixedLength() throws IOException { + testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH); + } + + @Test public void flushAfterStreamTransmittedWithNoLengthHeaders() throws IOException { + testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM); + } + + /** + * We explicitly permit apps to close the upload stream even after it has + * been transmitted. We also permit flush so that buffered streams can + * do a no-op flush when they are closed. http://b/3038470 + */ + private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException { + server.enqueue(new MockResponse().setBody("abc")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setDoOutput(true); + byte[] upload = "def".getBytes("UTF-8"); + + if (transferKind == TransferKind.CHUNKED) { + connection.setChunkedStreamingMode(0); + } else if (transferKind == TransferKind.FIXED_LENGTH) { + connection.setFixedLengthStreamingMode(upload.length); } - @Test public void setChunkedEncodingAsRequestProperty() throws IOException, InterruptedException { - server.enqueue(new MockResponse()); - server.play(); + OutputStream out = connection.getOutputStream(); + out.write(upload); + assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - HttpURLConnection urlConnection = client.open(server.getUrl("/")); - urlConnection.setRequestProperty("Transfer-encoding", "chunked"); - urlConnection.setDoOutput(true); - urlConnection.getOutputStream().write("ABC".getBytes("UTF-8")); - assertEquals(200, urlConnection.getResponseCode()); + out.flush(); // dubious but permitted + try { + out.write("ghi".getBytes("UTF-8")); + fail(); + } catch (IOException expected) { + } + } - RecordedRequest request = server.takeRequest(); - assertEquals("ABC", new String(request.getBody(), "UTF-8")); + @Test public void getHeadersThrows() throws IOException { + // Enqueue a response for every IP address held by localhost, because the route selector + // will try each in sequence. + // TODO: use the fake Dns implementation instead of a loop + for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) { + server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START)); + } + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + try { + connection.getInputStream(); + fail(); + } catch (IOException expected) { } - @Test public void connectionCloseInRequest() throws IOException, InterruptedException { - server.enqueue(new MockResponse()); // server doesn't honor the connection: close header! - server.enqueue(new MockResponse()); - server.play(); - - HttpURLConnection a = client.open(server.getUrl("/")); - a.setRequestProperty("Connection", "close"); - assertEquals(200, a.getResponseCode()); - - HttpURLConnection b = client.open(server.getUrl("/")); - assertEquals(200, b.getResponseCode()); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals("When connection: close is used, each request should get its own connection", - 0, server.takeRequest().getSequenceNumber()); + try { + connection.getInputStream(); + fail(); + } catch (IOException expected) { } + } - @Test public void connectionCloseInResponse() throws IOException, InterruptedException { - server.enqueue(new MockResponse().addHeader("Connection: close")); - server.enqueue(new MockResponse()); - server.play(); - - HttpURLConnection a = client.open(server.getUrl("/")); - assertEquals(200, a.getResponseCode()); - - HttpURLConnection b = client.open(server.getUrl("/")); - assertEquals(200, b.getResponseCode()); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals("When connection: close is used, each request should get its own connection", - 0, server.takeRequest().getSequenceNumber()); + @Test public void dnsFailureThrowsIOException() throws IOException { + HttpURLConnection connection = client.open(new URL("http://host.unlikelytld")); + try { + connection.connect(); + fail(); + } catch (IOException expected) { } + } - @Test public void connectionCloseWithRedirect() throws IOException, InterruptedException { - MockResponse response = new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: /foo") - .addHeader("Connection: close"); - server.enqueue(response); - server.enqueue(new MockResponse().setBody("This is the new location!")); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("This is the new location!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals("When connection: close is used, each request should get its own connection", - 0, server.takeRequest().getSequenceNumber()); + @Test public void malformedUrlThrowsUnknownHostException() throws IOException { + HttpURLConnection connection = client.open(new URL("http:///foo.html")); + try { + connection.connect(); + fail(); + } catch (UnknownHostException expected) { } + } - /** - * Retry redirects if the socket is closed. - * https://code.google.com/p/android/issues/detail?id=41576 - */ - @Test public void sameConnectionRedirectAndReuse() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .setSocketPolicy(SHUTDOWN_INPUT_AT_END) - .addHeader("Location: /foo")); - server.enqueue(new MockResponse().setBody("This is the new page!")); - server.play(); + @Test public void getKeepAlive() throws Exception { + MockWebServer server = new MockWebServer(); + server.enqueue(new MockResponse().setBody("ABC")); + server.play(); - assertContent("This is the new page!", client.open(server.getUrl("/"))); - - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals(0, server.takeRequest().getSequenceNumber()); + // The request should work once and then fail + URLConnection connection1 = client.open(server.getUrl("")); + connection1.setReadTimeout(100); + InputStream input = connection1.getInputStream(); + assertEquals("ABC", readAscii(input, Integer.MAX_VALUE)); + input.close(); + server.shutdown(); + try { + HttpURLConnection connection2 = client.open(server.getUrl("")); + connection2.setReadTimeout(100); + connection2.getInputStream(); + fail(); + } catch (ConnectException expected) { } + } - @Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT) - .setBody("This body is not allowed!")); - server.play(); + /** Don't explode if the cache returns a null body. http://b/3373699 */ + @Test public void responseCacheReturnsNullOutputStream() throws Exception { + final AtomicBoolean aborted = new AtomicBoolean(); + client.setResponseCache(new ResponseCache() { + @Override + public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + return null; + } - URLConnection connection = client.open(server.getUrl("/")); - assertEquals("This body is not allowed!", - readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - } + @Override + public CacheRequest put(URI uri, URLConnection connection) throws IOException { + return new CacheRequest() { + @Override + public void abort() { + aborted.set(true); + } - @Test public void singleByteReadIsSigned() throws IOException { - server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 })); - server.play(); - - URLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals(254, in.read()); - assertEquals(255, in.read()); - assertEquals(-1, in.read()); - } - - @Test public void flushAfterStreamTransmittedWithChunkedEncoding() throws IOException { - testFlushAfterStreamTransmitted(TransferKind.CHUNKED); - } - - @Test public void flushAfterStreamTransmittedWithFixedLength() throws IOException { - testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH); - } - - @Test public void flushAfterStreamTransmittedWithNoLengthHeaders() throws IOException { - testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM); - } - - /** - * We explicitly permit apps to close the upload stream even after it has - * been transmitted. We also permit flush so that buffered streams can - * do a no-op flush when they are closed. http://b/3038470 - */ - private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException { - server.enqueue(new MockResponse().setBody("abc")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setDoOutput(true); - byte[] upload = "def".getBytes("UTF-8"); - - if (transferKind == TransferKind.CHUNKED) { - connection.setChunkedStreamingMode(0); - } else if (transferKind == TransferKind.FIXED_LENGTH) { - connection.setFixedLengthStreamingMode(upload.length); - } - - OutputStream out = connection.getOutputStream(); - out.write(upload); - assertEquals("abc", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - out.flush(); // dubious but permitted - try { - out.write("ghi".getBytes("UTF-8")); - fail(); - } catch (IOException expected) { - } - } - - @Test public void getHeadersThrows() throws IOException { - // Enqueue a response for every IP address held by localhost, because the route selector - // will try each in sequence. - // TODO: use the fake Dns implementation instead of a loop - for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) { - server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START)); - } - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - try { - connection.getInputStream(); - fail(); - } catch (IOException expected) { - } - - try { - connection.getInputStream(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void dnsFailureThrowsIOException() throws IOException { - HttpURLConnection connection = client.open(new URL("http://host.unlikelytld")); - try { - connection.connect(); - fail(); - } catch (IOException expected) { - } - } - - @Test public void malformedUrlThrowsUnknownHostException() throws IOException { - HttpURLConnection connection = client.open(new URL("http:///foo.html")); - try { - connection.connect(); - fail(); - } catch (UnknownHostException expected) { - } - } - - @Test public void getKeepAlive() throws Exception { - MockWebServer server = new MockWebServer(); - server.enqueue(new MockResponse().setBody("ABC")); - server.play(); - - // The request should work once and then fail - URLConnection connection1 = client.open(server.getUrl("")); - connection1.setReadTimeout(100); - InputStream input = connection1.getInputStream(); - assertEquals("ABC", readAscii(input, Integer.MAX_VALUE)); - input.close(); - server.shutdown(); - try { - HttpURLConnection connection2 = client.open(server.getUrl("")); - connection2.setReadTimeout(100); - connection2.getInputStream(); - fail(); - } catch (ConnectException expected) { - } - } - - /** - * Don't explode if the cache returns a null body. http://b/3373699 - */ - @Test public void responseCacheReturnsNullOutputStream() throws Exception { - final AtomicBoolean aborted = new AtomicBoolean(); - client.setResponseCache(new ResponseCache() { - @Override - public CacheResponse get(URI uri, String requestMethod, - Map> requestHeaders) throws IOException { - return null; - } - - @Override - public CacheRequest put(URI uri, URLConnection connection) throws IOException { - return new CacheRequest() { - @Override - public void abort() { - aborted.set(true); - } - - @Override - public OutputStream getBody() throws IOException { - return null; - } - }; - } - }); - - server.enqueue(new MockResponse().setBody("abcdef")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals("abc", readAscii(in, 3)); - in.close(); - assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here - } - - /** - * http://code.google.com/p/android/issues/detail?id=14562 - */ - @Test public void readAfterLastByte() throws Exception { - server.enqueue(new MockResponse() - .setBody("ABC") - .clearHeaders() - .addHeader("Connection: close") - .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals("ABC", readAscii(in, 3)); - assertEquals(-1, in.read()); - assertEquals(-1, in.read()); // throws IOException in Gingerbread - } - - @Test public void getContent() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Content-Type: text/plain") - .setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - InputStream in = (InputStream) connection.getContent(); - assertEquals("A", readAscii(in, Integer.MAX_VALUE)); - } - - @Test public void getContentOfType() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Content-Type: text/plain") - .setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - try { - connection.getContent(null); - fail(); - } catch (NullPointerException expected) { - } - try { - connection.getContent(new Class[] { null }); - fail(); - } catch (NullPointerException expected) { - } - assertNull(connection.getContent(new Class[] { getClass() })); - connection.disconnect(); - } - - @Test public void getOutputStreamOnGetFails() throws Exception { - server.enqueue(new MockResponse()); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - try { - connection.getOutputStream(); - fail(); - } catch (ProtocolException expected) { - } - } - - @Test public void getOutputAfterGetInputStreamFails() throws Exception { - server.enqueue(new MockResponse()); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setDoOutput(true); - try { - connection.getInputStream(); - connection.getOutputStream(); - fail(); - } catch (ProtocolException expected) { - } - } - - @Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception { - server.enqueue(new MockResponse()); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.connect(); - try { - connection.setDoOutput(true); - fail(); - } catch (IllegalStateException expected) { - } - try { - connection.setDoInput(true); - fail(); - } catch (IllegalStateException expected) { - } - connection.disconnect(); - } - - @Test public void clientSendsContentLength() throws Exception { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - connection.setDoOutput(true); - OutputStream out = connection.getOutputStream(); - out.write(new byte[] { 'A', 'B', 'C' }); - out.close(); - assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - RecordedRequest request = server.takeRequest(); - assertContains(request.getHeaders(), "Content-Length: 3"); - } - - @Test public void getContentLengthConnects() throws Exception { - server.enqueue(new MockResponse().setBody("ABC")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals(3, connection.getContentLength()); - connection.disconnect(); - } - - @Test public void getContentTypeConnects() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Content-Type: text/plain") - .setBody("ABC")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("text/plain", connection.getContentType()); - connection.disconnect(); - } - - @Test public void getContentEncodingConnects() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Content-Encoding: identity") - .setBody("ABC")); - server.play(); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("identity", connection.getContentEncoding()); - connection.disconnect(); - } - - // http://b/4361656 - @Test public void urlContainsQueryButNoPath() throws Exception { - server.enqueue(new MockResponse().setBody("A")); - server.play(); - URL url = new URL("http", server.getHostName(), server.getPort(), "?query"); - assertEquals("A", readAscii(client.open(url).getInputStream(), Integer.MAX_VALUE)); - RecordedRequest request = server.takeRequest(); - assertEquals("GET /?query HTTP/1.1", request.getRequestLine()); - } - - // http://code.google.com/p/android/issues/detail?id=20442 - @Test public void inputStreamAvailableWithChunkedEncoding() throws Exception { - testInputStreamAvailable(TransferKind.CHUNKED); - } - - @Test public void inputStreamAvailableWithContentLengthHeader() throws Exception { - testInputStreamAvailable(TransferKind.FIXED_LENGTH); - } - - @Test public void inputStreamAvailableWithNoLengthHeaders() throws Exception { - testInputStreamAvailable(TransferKind.END_OF_STREAM); - } - - private void testInputStreamAvailable(TransferKind transferKind) throws IOException { - String body = "ABCDEFGH"; - MockResponse response = new MockResponse(); - transferKind.setBody(response, body, 4); - server.enqueue(response); - server.play(); - URLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - for (int i = 0; i < body.length(); i++) { - assertTrue(in.available() >= 0); - assertEquals(body.charAt(i), in.read()); - } - assertEquals(0, in.available()); - assertEquals(-1, in.read()); - } - - @Test @Ignore public void testPooledConnectionsDetectHttp10() { - // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1) - fail("TODO"); - } - - @Test @Ignore public void postBodiesRetransmittedOnAuthProblems() { - fail("TODO"); - } - - @Test @Ignore public void cookiesAndTrailers() { - // Do cookie headers get processed too many times? - fail("TODO"); - } - - @Test @Ignore public void headerNamesContainingNullCharacter() { - // This is relevant for SPDY - fail("TODO"); - } - - @Test @Ignore public void headerValuesContainingNullCharacter() { - // This is relevant for SPDY - fail("TODO"); - } - - @Test @Ignore public void emptyHeaderName() { - // This is relevant for SPDY - fail("TODO"); - } - - @Test @Ignore public void emptyHeaderValue() { - // This is relevant for SPDY - fail("TODO"); - } - - @Test @Ignore public void deflateCompression() { - fail("TODO"); - } - - @Test @Ignore public void postBodiesRetransmittedOnIpAddressProblems() { - fail("TODO"); - } - - @Test @Ignore public void pooledConnectionProblemsNotReportedToProxySelector() { - fail("TODO"); - } - - /** - * Returns a gzipped copy of {@code bytes}. - */ - public byte[] gzip(byte[] bytes) throws IOException { - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - OutputStream gzippedOut = new GZIPOutputStream(bytesOut); - gzippedOut.write(bytes); - gzippedOut.close(); - return bytesOut.toByteArray(); - } - - /** - * Reads at most {@code limit} characters from {@code in} and asserts that - * content equals {@code expected}. - */ - private void assertContent(String expected, URLConnection connection, int limit) - throws IOException { - connection.connect(); - assertEquals(expected, readAscii(connection.getInputStream(), limit)); - ((HttpURLConnection) connection).disconnect(); - } - - private void assertContent(String expected, URLConnection connection) throws IOException { - assertContent(expected, connection, Integer.MAX_VALUE); - } - - private void assertContains(List headers, String header) { - assertTrue(headers.toString(), headers.contains(header)); - } - - private void assertContainsNoneMatching(List headers, String pattern) { - for (String header : headers) { - if (header.matches(pattern)) { - fail("Header " + header + " matches " + pattern); - } - } - } - - private Set newSet(String... elements) { - return new HashSet(Arrays.asList(elements)); - } - - enum TransferKind { - CHUNKED() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) - throws IOException { - response.setChunkedBody(content, chunkSize); - } - @Override void setForRequest(HttpURLConnection connection, int contentLength) { - connection.setChunkedStreamingMode(5); - } - }, - FIXED_LENGTH() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) { - response.setBody(content); - } - @Override void setForRequest(HttpURLConnection connection, int contentLength) { - connection.setChunkedStreamingMode(contentLength); - } - }, - END_OF_STREAM() { - @Override void setBody(MockResponse response, byte[] content, int chunkSize) { - response.setBody(content); - response.setSocketPolicy(DISCONNECT_AT_END); - for (Iterator h = response.getHeaders().iterator(); h.hasNext(); ) { - if (h.next().startsWith("Content-Length:")) { - h.remove(); - break; - } - } - } - @Override void setForRequest(HttpURLConnection connection, int contentLength) { - } + @Override + public OutputStream getBody() throws IOException { + return null; + } }; + } + }); - abstract void setBody(MockResponse response, byte[] content, int chunkSize) - throws IOException; + server.enqueue(new MockResponse().setBody("abcdef")); + server.play(); - abstract void setForRequest(HttpURLConnection connection, int contentLength); + HttpURLConnection connection = client.open(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals("abc", readAscii(in, 3)); + in.close(); + assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here + } - void setBody(MockResponse response, String content, int chunkSize) throws IOException { - setBody(response, content.getBytes("UTF-8"), chunkSize); + /** http://code.google.com/p/android/issues/detail?id=14562 */ + @Test public void readAfterLastByte() throws Exception { + server.enqueue(new MockResponse().setBody("ABC") + .clearHeaders() + .addHeader("Connection: close") + .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals("ABC", readAscii(in, 3)); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); // throws IOException in Gingerbread + } + + @Test public void getContent() throws Exception { + server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + InputStream in = (InputStream) connection.getContent(); + assertEquals("A", readAscii(in, Integer.MAX_VALUE)); + } + + @Test public void getContentOfType() throws Exception { + server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + try { + connection.getContent(null); + fail(); + } catch (NullPointerException expected) { + } + try { + connection.getContent(new Class[] { null }); + fail(); + } catch (NullPointerException expected) { + } + assertNull(connection.getContent(new Class[] { getClass() })); + connection.disconnect(); + } + + @Test public void getOutputStreamOnGetFails() throws Exception { + server.enqueue(new MockResponse()); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + try { + connection.getOutputStream(); + fail(); + } catch (ProtocolException expected) { + } + } + + @Test public void getOutputAfterGetInputStreamFails() throws Exception { + server.enqueue(new MockResponse()); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setDoOutput(true); + try { + connection.getInputStream(); + connection.getOutputStream(); + fail(); + } catch (ProtocolException expected) { + } + } + + @Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception { + server.enqueue(new MockResponse()); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.connect(); + try { + connection.setDoOutput(true); + fail(); + } catch (IllegalStateException expected) { + } + try { + connection.setDoInput(true); + fail(); + } catch (IllegalStateException expected) { + } + connection.disconnect(); + } + + @Test public void clientSendsContentLength() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + connection.setDoOutput(true); + OutputStream out = connection.getOutputStream(); + out.write(new byte[] { 'A', 'B', 'C' }); + out.close(); + assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + RecordedRequest request = server.takeRequest(); + assertContains(request.getHeaders(), "Content-Length: 3"); + } + + @Test public void getContentLengthConnects() throws Exception { + server.enqueue(new MockResponse().setBody("ABC")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals(3, connection.getContentLength()); + connection.disconnect(); + } + + @Test public void getContentTypeConnects() throws Exception { + server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("ABC")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("text/plain", connection.getContentType()); + connection.disconnect(); + } + + @Test public void getContentEncodingConnects() throws Exception { + server.enqueue(new MockResponse().addHeader("Content-Encoding: identity").setBody("ABC")); + server.play(); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("identity", connection.getContentEncoding()); + connection.disconnect(); + } + + // http://b/4361656 + @Test public void urlContainsQueryButNoPath() throws Exception { + server.enqueue(new MockResponse().setBody("A")); + server.play(); + URL url = new URL("http", server.getHostName(), server.getPort(), "?query"); + assertEquals("A", readAscii(client.open(url).getInputStream(), Integer.MAX_VALUE)); + RecordedRequest request = server.takeRequest(); + assertEquals("GET /?query HTTP/1.1", request.getRequestLine()); + } + + // http://code.google.com/p/android/issues/detail?id=20442 + @Test public void inputStreamAvailableWithChunkedEncoding() throws Exception { + testInputStreamAvailable(TransferKind.CHUNKED); + } + + @Test public void inputStreamAvailableWithContentLengthHeader() throws Exception { + testInputStreamAvailable(TransferKind.FIXED_LENGTH); + } + + @Test public void inputStreamAvailableWithNoLengthHeaders() throws Exception { + testInputStreamAvailable(TransferKind.END_OF_STREAM); + } + + private void testInputStreamAvailable(TransferKind transferKind) throws IOException { + String body = "ABCDEFGH"; + MockResponse response = new MockResponse(); + transferKind.setBody(response, body, 4); + server.enqueue(response); + server.play(); + URLConnection connection = client.open(server.getUrl("/")); + InputStream in = connection.getInputStream(); + for (int i = 0; i < body.length(); i++) { + assertTrue(in.available() >= 0); + assertEquals(body.charAt(i), in.read()); + } + assertEquals(0, in.available()); + assertEquals(-1, in.read()); + } + + @Test @Ignore public void testPooledConnectionsDetectHttp10() { + // TODO: write a test that shows pooled connections detect HTTP/1.0 (vs. HTTP/1.1) + fail("TODO"); + } + + @Test @Ignore public void postBodiesRetransmittedOnAuthProblems() { + fail("TODO"); + } + + @Test @Ignore public void cookiesAndTrailers() { + // Do cookie headers get processed too many times? + fail("TODO"); + } + + @Test @Ignore public void headerNamesContainingNullCharacter() { + // This is relevant for SPDY + fail("TODO"); + } + + @Test @Ignore public void headerValuesContainingNullCharacter() { + // This is relevant for SPDY + fail("TODO"); + } + + @Test @Ignore public void emptyHeaderName() { + // This is relevant for SPDY + fail("TODO"); + } + + @Test @Ignore public void emptyHeaderValue() { + // This is relevant for SPDY + fail("TODO"); + } + + @Test @Ignore public void deflateCompression() { + fail("TODO"); + } + + @Test @Ignore public void postBodiesRetransmittedOnIpAddressProblems() { + fail("TODO"); + } + + @Test @Ignore public void pooledConnectionProblemsNotReportedToProxySelector() { + fail("TODO"); + } + + /** Returns a gzipped copy of {@code bytes}. */ + public byte[] gzip(byte[] bytes) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + OutputStream gzippedOut = new GZIPOutputStream(bytesOut); + gzippedOut.write(bytes); + gzippedOut.close(); + return bytesOut.toByteArray(); + } + + /** + * Reads at most {@code limit} characters from {@code in} and asserts that + * content equals {@code expected}. + */ + private void assertContent(String expected, URLConnection connection, int limit) + throws IOException { + connection.connect(); + assertEquals(expected, readAscii(connection.getInputStream(), limit)); + ((HttpURLConnection) connection).disconnect(); + } + + private void assertContent(String expected, URLConnection connection) throws IOException { + assertContent(expected, connection, Integer.MAX_VALUE); + } + + private void assertContains(List headers, String header) { + assertTrue(headers.toString(), headers.contains(header)); + } + + private void assertContainsNoneMatching(List headers, String pattern) { + for (String header : headers) { + if (header.matches(pattern)) { + fail("Header " + header + " matches " + pattern); + } + } + } + + private Set newSet(String... elements) { + return new HashSet(Arrays.asList(elements)); + } + + enum TransferKind { + CHUNKED() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) + throws IOException { + response.setChunkedBody(content, chunkSize); + } + @Override void setForRequest(HttpURLConnection connection, int contentLength) { + connection.setChunkedStreamingMode(5); + } + }, + FIXED_LENGTH() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) { + response.setBody(content); + } + @Override void setForRequest(HttpURLConnection connection, int contentLength) { + connection.setChunkedStreamingMode(contentLength); + } + }, + END_OF_STREAM() { + @Override void setBody(MockResponse response, byte[] content, int chunkSize) { + response.setBody(content); + response.setSocketPolicy(DISCONNECT_AT_END); + for (Iterator h = response.getHeaders().iterator(); h.hasNext(); ) { + if (h.next().startsWith("Content-Length:")) { + h.remove(); + break; + } } + } + @Override void setForRequest(HttpURLConnection connection, int contentLength) { + } + }; + + abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException; + + abstract void setForRequest(HttpURLConnection connection, int contentLength); + + void setBody(MockResponse response, String content, int chunkSize) throws IOException { + setBody(response, content.getBytes("UTF-8"), chunkSize); + } + } + + enum ProxyConfig { + NO_PROXY() { + @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) + throws IOException { + client.setProxy(Proxy.NO_PROXY); + return client.open(url); + } + }, + + CREATE_ARG() { + @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) + throws IOException { + client.setProxy(server.toProxyAddress()); + return client.open(url); + } + }, + + PROXY_SYSTEM_PROPERTY() { + @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) + throws IOException { + System.setProperty("proxyHost", "localhost"); + System.setProperty("proxyPort", Integer.toString(server.getPort())); + return client.open(url); + } + }, + + HTTP_PROXY_SYSTEM_PROPERTY() { + @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) + throws IOException { + System.setProperty("http.proxyHost", "localhost"); + System.setProperty("http.proxyPort", Integer.toString(server.getPort())); + return client.open(url); + } + }, + + HTTPS_PROXY_SYSTEM_PROPERTY() { + @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) + throws IOException { + System.setProperty("https.proxyHost", "localhost"); + System.setProperty("https.proxyPort", Integer.toString(server.getPort())); + return client.open(url); + } + }; + + public abstract HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) + throws IOException; + } + + private static class RecordingTrustManager implements X509TrustManager { + private final List calls = new ArrayList(); + + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[] { }; } - enum ProxyConfig { - NO_PROXY() { - @Override public HttpURLConnection connect - (MockWebServer server, OkHttpClient client, URL url) throws IOException { - client.setProxy(Proxy.NO_PROXY); - return client.open(url); - } - }, - - CREATE_ARG() { - @Override public HttpURLConnection connect( - MockWebServer server, OkHttpClient client, URL url) throws IOException { - client.setProxy(server.toProxyAddress()); - return client.open(url); - } - }, - - PROXY_SYSTEM_PROPERTY() { - @Override public HttpURLConnection connect( - MockWebServer server, OkHttpClient client, URL url) throws IOException { - System.setProperty("proxyHost", "localhost"); - System.setProperty("proxyPort", Integer.toString(server.getPort())); - return client.open(url); - } - }, - - HTTP_PROXY_SYSTEM_PROPERTY() { - @Override public HttpURLConnection connect( - MockWebServer server, OkHttpClient client, URL url) throws IOException { - System.setProperty("http.proxyHost", "localhost"); - System.setProperty("http.proxyPort", Integer.toString(server.getPort())); - return client.open(url); - } - }, - - HTTPS_PROXY_SYSTEM_PROPERTY() { - @Override public HttpURLConnection connect( - MockWebServer server, OkHttpClient client, URL url) throws IOException { - System.setProperty("https.proxyHost", "localhost"); - System.setProperty("https.proxyPort", Integer.toString(server.getPort())); - return client.open(url); - } - }; - - public abstract HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url) throws IOException; + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + calls.add("checkClientTrusted " + certificatesToString(chain)); } - private static class RecordingTrustManager implements X509TrustManager { - private final List calls = new ArrayList(); - - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[] {}; - } - - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - calls.add("checkClientTrusted " + certificatesToString(chain)); - } - - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - calls.add("checkServerTrusted " + certificatesToString(chain)); - } - - private String certificatesToString(X509Certificate[] certificates) { - List result = new ArrayList(); - for (X509Certificate certificate : certificates) { - result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber()); - } - return result.toString(); - } + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + calls.add("checkServerTrusted " + certificatesToString(chain)); } - private static class FakeProxySelector extends ProxySelector { - List proxies = new ArrayList(); - - @Override public List select(URI uri) { - // Don't handle 'socket' schemes, which the RI's Socket class may request (for SOCKS). - return uri.getScheme().equals("http") || uri.getScheme().equals("https") - ? proxies - : Collections.singletonList(Proxy.NO_PROXY); - } - - @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { - } + private String certificatesToString(X509Certificate[] certificates) { + List result = new ArrayList(); + for (X509Certificate certificate : certificates) { + result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber()); + } + return result.toString(); } + } + + private static class FakeProxySelector extends ProxySelector { + List proxies = new ArrayList(); + + @Override public List select(URI uri) { + // Don't handle 'socket' schemes, which the RI's Socket class may request (for SOCKS). + return uri.getScheme().equals("http") || uri.getScheme().equals("https") ? proxies + : Collections.singletonList(Proxy.NO_PROXY); + } + + @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) { + } + } } diff --git a/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java b/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java index 0f8edcab7..6ca375691 100644 --- a/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java +++ b/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java @@ -29,122 +29,122 @@ import java.net.URLConnection; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import static org.junit.Assert.assertEquals; import org.junit.Ignore; import org.junit.Test; +import static org.junit.Assert.assertEquals; + /** * Exercises HttpURLConnection to convert URL to a URI. Unlike URL#toURI, * HttpURLConnection recovers from URLs with unescaped but unsupported URI * characters like '{' and '|' by escaping these characters. */ public final class URLEncodingTest { - /** - * This test goes through the exhaustive set of interesting ASCII characters - * because most of those characters are interesting in some way according to - * RFC 2396 and RFC 2732. http://b/1158780 - */ - @Test @Ignore public void lenientUrlToUri() throws Exception { - // alphanum - testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09"); + /** + * This test goes through the exhaustive set of interesting ASCII characters + * because most of those characters are interesting in some way according to + * RFC 2396 and RFC 2732. http://b/1158780 + */ + @Test @Ignore public void lenientUrlToUri() throws Exception { + // alphanum + testUrlToUriMapping("abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09", "abzABZ09"); - // control characters - testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01"); - testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F"); + // control characters + testUrlToUriMapping("\u0001", "%01", "%01", "%01", "%01"); + testUrlToUriMapping("\u001f", "%1F", "%1F", "%1F", "%1F"); - // ascii characters - testUrlToUriMapping("%20", "%20", "%20", "%20", "%20"); - testUrlToUriMapping("%20", "%20", "%20", "%20", "%20"); - testUrlToUriMapping(" ", "%20", "%20", "%20", "%20"); - testUrlToUriMapping("!", "!", "!", "!", "!"); - testUrlToUriMapping("\"", "%22", "%22", "%22", "%22"); - testUrlToUriMapping("#", null, null, null, "%23"); - testUrlToUriMapping("$", "$", "$", "$", "$"); - testUrlToUriMapping("&", "&", "&", "&", "&"); - testUrlToUriMapping("'", "'", "'", "'", "'"); - testUrlToUriMapping("(", "(", "(", "(", "("); - testUrlToUriMapping(")", ")", ")", ")", ")"); - testUrlToUriMapping("*", "*", "*", "*", "*"); - testUrlToUriMapping("+", "+", "+", "+", "+"); - testUrlToUriMapping(",", ",", ",", ",", ","); - testUrlToUriMapping("-", "-", "-", "-", "-"); - testUrlToUriMapping(".", ".", ".", ".", "."); - testUrlToUriMapping("/", null, "/", "/", "/"); - testUrlToUriMapping(":", null, ":", ":", ":"); - testUrlToUriMapping(";", ";", ";", ";", ";"); - testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C"); - testUrlToUriMapping("=", "=", "=", "=", "="); - testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E"); - testUrlToUriMapping("?", null, null, "?", "?"); - testUrlToUriMapping("@", "@", "@", "@", "@"); - testUrlToUriMapping("[", null, "%5B", null, "%5B"); - testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C"); - testUrlToUriMapping("]", null, "%5D", null, "%5D"); - testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E"); - testUrlToUriMapping("_", "_", "_", "_", "_"); - testUrlToUriMapping("`", "%60", "%60", "%60", "%60"); - testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B"); - testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C"); - testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D"); - testUrlToUriMapping("~", "~", "~", "~", "~"); - testUrlToUriMapping("~", "~", "~", "~", "~"); - testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F"); + // ascii characters + testUrlToUriMapping("%20", "%20", "%20", "%20", "%20"); + testUrlToUriMapping("%20", "%20", "%20", "%20", "%20"); + testUrlToUriMapping(" ", "%20", "%20", "%20", "%20"); + testUrlToUriMapping("!", "!", "!", "!", "!"); + testUrlToUriMapping("\"", "%22", "%22", "%22", "%22"); + testUrlToUriMapping("#", null, null, null, "%23"); + testUrlToUriMapping("$", "$", "$", "$", "$"); + testUrlToUriMapping("&", "&", "&", "&", "&"); + testUrlToUriMapping("'", "'", "'", "'", "'"); + testUrlToUriMapping("(", "(", "(", "(", "("); + testUrlToUriMapping(")", ")", ")", ")", ")"); + testUrlToUriMapping("*", "*", "*", "*", "*"); + testUrlToUriMapping("+", "+", "+", "+", "+"); + testUrlToUriMapping(",", ",", ",", ",", ","); + testUrlToUriMapping("-", "-", "-", "-", "-"); + testUrlToUriMapping(".", ".", ".", ".", "."); + testUrlToUriMapping("/", null, "/", "/", "/"); + testUrlToUriMapping(":", null, ":", ":", ":"); + testUrlToUriMapping(";", ";", ";", ";", ";"); + testUrlToUriMapping("<", "%3C", "%3C", "%3C", "%3C"); + testUrlToUriMapping("=", "=", "=", "=", "="); + testUrlToUriMapping(">", "%3E", "%3E", "%3E", "%3E"); + testUrlToUriMapping("?", null, null, "?", "?"); + testUrlToUriMapping("@", "@", "@", "@", "@"); + testUrlToUriMapping("[", null, "%5B", null, "%5B"); + testUrlToUriMapping("\\", "%5C", "%5C", "%5C", "%5C"); + testUrlToUriMapping("]", null, "%5D", null, "%5D"); + testUrlToUriMapping("^", "%5E", "%5E", "%5E", "%5E"); + testUrlToUriMapping("_", "_", "_", "_", "_"); + testUrlToUriMapping("`", "%60", "%60", "%60", "%60"); + testUrlToUriMapping("{", "%7B", "%7B", "%7B", "%7B"); + testUrlToUriMapping("|", "%7C", "%7C", "%7C", "%7C"); + testUrlToUriMapping("}", "%7D", "%7D", "%7D", "%7D"); + testUrlToUriMapping("~", "~", "~", "~", "~"); + testUrlToUriMapping("~", "~", "~", "~", "~"); + testUrlToUriMapping("\u007f", "%7F", "%7F", "%7F", "%7F"); - // beyond ascii - testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80"); - testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac"); - testUrlToUriMapping("\ud842\udf9f", - "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f"); + // beyond ascii + testUrlToUriMapping("\u0080", "%C2%80", "%C2%80", "%C2%80", "%C2%80"); + testUrlToUriMapping("\u20ac", "\u20ac", "\u20ac", "\u20ac", "\u20ac"); + testUrlToUriMapping("\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", "\ud842\udf9f", + "\ud842\udf9f"); + } + + @Test @Ignore public void lenientUrlToUriNul() throws Exception { + testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this + } + + private void testUrlToUriMapping(String string, String asAuthority, String asFile, String asQuery, + String asFragment) throws Exception { + if (asAuthority != null) { + assertEquals("http://host" + asAuthority + ".tld/", + backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString()); + } + if (asFile != null) { + assertEquals("http://host.tld/file" + asFile + "/", + backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString()); + } + if (asQuery != null) { + assertEquals("http://host.tld/file?q" + asQuery + "=x", + backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString()); + } + assertEquals("http://host.tld/file#" + asFragment + "-x", + backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString()); + } + + private URI backdoorUrlToUri(URL url) throws Exception { + final AtomicReference uriReference = new AtomicReference(); + + OkHttpClient client = new OkHttpClient(); + client.setResponseCache(new ResponseCache() { + @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException { + return null; + } + + @Override public CacheResponse get(URI uri, String requestMethod, + Map> requestHeaders) throws IOException { + uriReference.set(uri); + throw new UnsupportedOperationException(); + } + }); + + try { + HttpURLConnection connection = client.open(url); + connection.getResponseCode(); + } catch (Exception expected) { + if (expected.getCause() instanceof URISyntaxException) { + expected.printStackTrace(); + } } - @Test @Ignore public void lenientUrlToUriNul() throws Exception { - testUrlToUriMapping("\u0000", "%00", "%00", "%00", "%00"); // RI fails this - } - - private void testUrlToUriMapping(String string, String asAuthority, String asFile, - String asQuery, String asFragment) throws Exception { - if (asAuthority != null) { - assertEquals("http://host" + asAuthority + ".tld/", - backdoorUrlToUri(new URL("http://host" + string + ".tld/")).toString()); - } - if (asFile != null) { - assertEquals("http://host.tld/file" + asFile + "/", - backdoorUrlToUri(new URL("http://host.tld/file" + string + "/")).toString()); - } - if (asQuery != null) { - assertEquals("http://host.tld/file?q" + asQuery + "=x", - backdoorUrlToUri(new URL("http://host.tld/file?q" + string + "=x")).toString()); - } - assertEquals("http://host.tld/file#" + asFragment + "-x", - backdoorUrlToUri(new URL("http://host.tld/file#" + asFragment + "-x")).toString()); - } - - private URI backdoorUrlToUri(URL url) throws Exception { - final AtomicReference uriReference = new AtomicReference(); - - OkHttpClient client = new OkHttpClient(); - client.setResponseCache(new ResponseCache() { - @Override public CacheRequest put(URI uri, URLConnection connection) - throws IOException { - return null; - } - - @Override public CacheResponse get(URI uri, String requestMethod, - Map> requestHeaders) throws IOException { - uriReference.set(uri); - throw new UnsupportedOperationException(); - } - }); - - try { - HttpURLConnection connection = client.open(url); - connection.getResponseCode(); - } catch (Exception expected) { - if (expected.getCause() instanceof URISyntaxException) { - expected.printStackTrace(); - } - } - - return uriReference.get(); - } + return uriReference.get(); + } } diff --git a/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java b/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java index 5380c56b8..3ee9543b8 100644 --- a/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java +++ b/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java @@ -48,222 +48,218 @@ import java.util.logging.Logger; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; -/** - * A scriptable spdy/3 + HTTP server. - */ +/** A scriptable spdy/3 + HTTP server. */ public final class MockSpdyServer { - private static final byte[] NPN_PROTOCOLS = new byte[] { - 6, 's', 'p', 'd', 'y', '/', '3', - }; - private static final Logger logger = Logger.getLogger(MockSpdyServer.class.getName()); - private SSLSocketFactory sslSocketFactory; - private QueueDispatcher dispatcher = new QueueDispatcher(); - private ServerSocket serverSocket; - private final Set openClientSockets - = Collections.newSetFromMap(new ConcurrentHashMap()); - private int port = -1; - private final BlockingQueue requestQueue - = new LinkedBlockingQueue(); + private static final byte[] NPN_PROTOCOLS = new byte[] { 6, 's', 'p', 'd', 'y', '/', '3', }; + private static final Logger logger = Logger.getLogger(MockSpdyServer.class.getName()); + private SSLSocketFactory sslSocketFactory; + private QueueDispatcher dispatcher = new QueueDispatcher(); + private ServerSocket serverSocket; + private final Set openClientSockets = + Collections.newSetFromMap(new ConcurrentHashMap()); + private int port = -1; + private final BlockingQueue requestQueue = + new LinkedBlockingQueue(); - public MockSpdyServer(SSLSocketFactory sslSocketFactory) { - this.sslSocketFactory = sslSocketFactory; + public MockSpdyServer(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + public String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + throw new AssertionError(); } + } - public String getHostName() { + public int getPort() { + if (port == -1) { + throw new IllegalStateException("Cannot retrieve port before calling play()"); + } + return port; + } + + public URL getUrl(String path) { + try { + return new URL("https://" + getHostName() + ":" + getPort() + path); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + } + + /** + * Returns a cookie domain for this server. This returns the server's + * non-loopback host name if it is known. Otherwise this returns ".local" + * for this server's loopback name. + */ + public String getCookieDomain() { + String hostName = getHostName(); + return hostName.contains(".") ? hostName : ".local"; + } + + /** + * Awaits the next HTTP request, removes it, and returns it. Callers should + * use this to verify the request sent was as intended. + */ + public RecordedRequest takeRequest() throws InterruptedException { + return requestQueue.take(); + } + + public void play() throws IOException { + serverSocket = new ServerSocket(0); + serverSocket.setReuseAddress(true); + port = serverSocket.getLocalPort(); + + Thread acceptThread = new Thread("MockSpdyServer-accept-" + port) { + @Override public void run() { + int sequenceNumber = 0; try { - return InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - throw new AssertionError(); + acceptConnections(sequenceNumber); + } catch (Throwable e) { + logger.log(Level.WARNING, "MockWebServer connection failed", e); } - } - - public int getPort() { - if (port == -1) { - throw new IllegalStateException("Cannot retrieve port before calling play()"); - } - return port; - } - - public URL getUrl(String path) { - try { - return new URL("https://" + getHostName() + ":" + getPort() + path); - } catch (MalformedURLException e) { - throw new AssertionError(e); - } - } - - /** - * Returns a cookie domain for this server. This returns the server's - * non-loopback host name if it is known. Otherwise this returns ".local" - * for this server's loopback name. - */ - public String getCookieDomain() { - String hostName = getHostName(); - return hostName.contains(".") ? hostName : ".local"; - } - - /** - * Awaits the next HTTP request, removes it, and returns it. Callers should - * use this to verify the request sent was as intended. - */ - public RecordedRequest takeRequest() throws InterruptedException { - return requestQueue.take(); - } - - public void play() throws IOException { - serverSocket = new ServerSocket(0); - serverSocket.setReuseAddress(true); - port = serverSocket.getLocalPort(); - - Thread acceptThread = new Thread("MockSpdyServer-accept-" + port) { - @Override public void run() { - int sequenceNumber = 0; - try { - acceptConnections(sequenceNumber); - } catch (Throwable e) { - logger.log(Level.WARNING, "MockWebServer connection failed", e); - } /* * This gnarly block of code will release all sockets and * all thread, even if any close fails. */ - try { - serverSocket.close(); - } catch (Throwable e) { - logger.log(Level.WARNING, "MockWebServer server socket close failed", e); - } - for (Iterator s = openClientSockets.iterator(); s.hasNext(); ) { - try { - s.next().close(); - s.remove(); - } catch (Throwable e) { - logger.log(Level.WARNING, "MockWebServer socket close failed", e); - } - } - } - }; - acceptThread.start(); + try { + serverSocket.close(); + } catch (Throwable e) { + logger.log(Level.WARNING, "MockWebServer server socket close failed", e); + } + for (Iterator s = openClientSockets.iterator(); s.hasNext(); ) { + try { + s.next().close(); + s.remove(); + } catch (Throwable e) { + logger.log(Level.WARNING, "MockWebServer socket close failed", e); + } + } + } + }; + acceptThread.start(); + } + + public void enqueue(MockResponse response) { + dispatcher.enqueueResponse(response); + } + + private void acceptConnections(int sequenceNumber) throws Exception { + while (true) { + Socket socket; + try { + socket = serverSocket.accept(); + } catch (SocketException e) { + return; + } + openClientSockets.add(socket); + new SocketHandler(sequenceNumber++, socket).serve(); + } + } + + public void shutdown() throws IOException { + if (serverSocket != null) { + serverSocket.close(); // should cause acceptConnections() to break out + } + } + + private class SocketHandler implements IncomingStreamHandler { + private final int sequenceNumber; + private Socket socket; + + private SocketHandler(int sequenceNumber, Socket socket) throws IOException { + this.socket = socket; + this.sequenceNumber = sequenceNumber; } - public void enqueue(MockResponse response) { - dispatcher.enqueueResponse(response); + public void serve() throws IOException { + if (sslSocketFactory != null) { + socket = doSsl(socket); + } + new SpdyConnection.Builder(false, socket).handler(this).build(); } - private void acceptConnections(int sequenceNumber) throws Exception { - while (true) { - Socket socket; - try { - socket = serverSocket.accept(); - } catch (SocketException e) { - return; - } - openClientSockets.add(socket); - new SocketHandler(sequenceNumber++, socket).serve(); - } + private Socket doSsl(Socket socket) throws IOException { + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, + socket.getInetAddress().getHostAddress(), socket.getPort(), true); + sslSocket.setUseClientMode(false); + Platform.get().setNpnProtocols(sslSocket, NPN_PROTOCOLS); + return sslSocket; } - public void shutdown() throws IOException { - if (serverSocket != null) { - serverSocket.close(); // should cause acceptConnections() to break out - } + @Override public void receive(final SpdyStream stream) throws IOException { + RecordedRequest request = readRequest(stream); + requestQueue.add(request); + MockResponse response; + try { + response = dispatcher.dispatch(request); + } catch (InterruptedException e) { + throw new AssertionError(e); + } + writeResponse(stream, response); + logger.info("Received request: " + request + " and responded: " + response); } - private class SocketHandler implements IncomingStreamHandler { - private final int sequenceNumber; - private Socket socket; - - private SocketHandler(int sequenceNumber, Socket socket) throws IOException { - this.socket = socket; - this.sequenceNumber = sequenceNumber; + private RecordedRequest readRequest(SpdyStream stream) throws IOException { + List spdyHeaders = stream.getRequestHeaders(); + List httpHeaders = new ArrayList(); + String method = "<:method omitted>"; + String path = "<:path omitted>"; + String version = "<:version omitted>"; + for (Iterator i = spdyHeaders.iterator(); i.hasNext(); ) { + String name = i.next(); + String value = i.next(); + if (":method".equals(name)) { + method = value; + } else if (":path".equals(name)) { + path = value; + } else if (":version".equals(name)) { + version = value; + } else { + httpHeaders.add(name + ": " + value); } + } - public void serve() throws IOException { - if (sslSocketFactory != null) { - socket = doSsl(socket); - } - new SpdyConnection.Builder(false, socket).handler(this).build(); - } - - private Socket doSsl(Socket socket) throws IOException { - SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, - socket.getInetAddress().getHostAddress(), socket.getPort(), true); - sslSocket.setUseClientMode(false); - Platform.get().setNpnProtocols(sslSocket, NPN_PROTOCOLS); - return sslSocket; - } - - @Override public void receive(final SpdyStream stream) throws IOException { - RecordedRequest request = readRequest(stream); - requestQueue.add(request); - MockResponse response; - try { - response = dispatcher.dispatch(request); - } catch (InterruptedException e) { - throw new AssertionError(e); - } - writeResponse(stream, response); - logger.info("Received request: " + request + " and responded: " + response); - } - - private RecordedRequest readRequest(SpdyStream stream) throws IOException { - List spdyHeaders = stream.getRequestHeaders(); - List httpHeaders = new ArrayList(); - String method = "<:method omitted>"; - String path = "<:path omitted>"; - String version = "<:version omitted>"; - for (Iterator i = spdyHeaders.iterator(); i.hasNext(); ) { - String name = i.next(); - String value = i.next(); - if (":method".equals(name)) { - method = value; - } else if (":path".equals(name)) { - path = value; - } else if (":version".equals(name)) { - version = value; - } else { - httpHeaders.add(name + ": " + value); - } - } - - InputStream bodyIn = stream.getInputStream(); - ByteArrayOutputStream bodyOut = new ByteArrayOutputStream(); - byte[] buffer = new byte[8192]; - int count; - while ((count = bodyIn.read(buffer)) != -1) { - bodyOut.write(buffer, 0, count); - } - bodyIn.close(); - String requestLine = method + ' ' + path + ' ' + version; - List chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY. - return new RecordedRequest(requestLine, httpHeaders, chunkSizes, bodyOut.size(), - bodyOut.toByteArray(), sequenceNumber, socket); - } - - private void writeResponse(SpdyStream stream, MockResponse response) throws IOException { - List spdyHeaders = new ArrayList(); - String[] statusParts = response.getStatus().split(" ", 2); - if (statusParts.length != 2) { - throw new AssertionError("Unexpected status: " + response.getStatus()); - } - spdyHeaders.add(":status"); - spdyHeaders.add(statusParts[1]); - spdyHeaders.add(":version"); - spdyHeaders.add(statusParts[0]); - for (String header : response.getHeaders()) { - String[] headerParts = header.split(":", 2); - if (headerParts.length != 2) { - throw new AssertionError("Unexpected header: " + header); - } - spdyHeaders.add(headerParts[0].toLowerCase(Locale.US).trim()); - spdyHeaders.add(headerParts[1].trim()); - } - byte[] body = response.getBody(); - stream.reply(spdyHeaders, body.length > 0); - if (body.length > 0) { - stream.getOutputStream().write(body); - stream.getOutputStream().close(); - } - } + InputStream bodyIn = stream.getInputStream(); + ByteArrayOutputStream bodyOut = new ByteArrayOutputStream(); + byte[] buffer = new byte[8192]; + int count; + while ((count = bodyIn.read(buffer)) != -1) { + bodyOut.write(buffer, 0, count); + } + bodyIn.close(); + String requestLine = method + ' ' + path + ' ' + version; + List chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY. + return new RecordedRequest(requestLine, httpHeaders, chunkSizes, bodyOut.size(), + bodyOut.toByteArray(), sequenceNumber, socket); } + + private void writeResponse(SpdyStream stream, MockResponse response) throws IOException { + List spdyHeaders = new ArrayList(); + String[] statusParts = response.getStatus().split(" ", 2); + if (statusParts.length != 2) { + throw new AssertionError("Unexpected status: " + response.getStatus()); + } + spdyHeaders.add(":status"); + spdyHeaders.add(statusParts[1]); + spdyHeaders.add(":version"); + spdyHeaders.add(statusParts[0]); + for (String header : response.getHeaders()) { + String[] headerParts = header.split(":", 2); + if (headerParts.length != 2) { + throw new AssertionError("Unexpected header: " + header); + } + spdyHeaders.add(headerParts[0].toLowerCase(Locale.US).trim()); + spdyHeaders.add(headerParts[1].trim()); + } + byte[] body = response.getBody(); + stream.reply(spdyHeaders, body.length > 0); + if (body.length > 0) { + stream.getOutputStream().write(body); + stream.getOutputStream().close(); + } + } + } } diff --git a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java index ced89c8d7..b8afeb2d8 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java @@ -47,235 +47,229 @@ import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import org.junit.After; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; import org.junit.Before; import org.junit.Test; -/** - * Test how SPDY interacts with HTTP features. - */ +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** Test how SPDY interacts with HTTP features. */ public final class HttpOverSpdyTest { - private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { - public boolean verify(String hostname, SSLSession session) { - return true; - } - }; - - private static final SSLContext sslContext; - static { - try { - sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); - } catch (GeneralSecurityException e) { - throw new RuntimeException(e); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } + private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() { + public boolean verify(String hostname, SSLSession session) { + return true; } - private final MockSpdyServer server = new MockSpdyServer(sslContext.getSocketFactory()); - private final String hostName = server.getHostName(); - private final OkHttpClient client = new OkHttpClient(); - private HttpResponseCache cache; + }; - @Before public void setUp() throws Exception { - client.setSSLSocketFactory(sslContext.getSocketFactory()); - client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); - String systemTmpDir = System.getProperty("java.io.tmpdir"); - File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID()); - cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); + private static final SSLContext sslContext; + static { + try { + sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); + } catch (UnknownHostException e) { + throw new RuntimeException(e); } + } + private final MockSpdyServer server = new MockSpdyServer(sslContext.getSocketFactory()); + private final String hostName = server.getHostName(); + private final OkHttpClient client = new OkHttpClient(); + private HttpResponseCache cache; - @After public void tearDown() throws Exception { - server.shutdown(); + @Before public void setUp() throws Exception { + client.setSSLSocketFactory(sslContext.getSocketFactory()); + client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER); + String systemTmpDir = System.getProperty("java.io.tmpdir"); + File cacheDir = new File(systemTmpDir, "HttpCache-" + UUID.randomUUID()); + cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE); + } + + @After public void tearDown() throws Exception { + server.shutdown(); + } + + @Test public void get() throws Exception { + MockResponse response = new MockResponse().setBody("ABCDE"); + server.enqueue(response); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/foo")); + assertContent("ABCDE", connection, Integer.MAX_VALUE); + + RecordedRequest request = server.takeRequest(); + assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); + assertContains(request.getHeaders(), ":scheme: https"); + assertContains(request.getHeaders(), ":host: " + hostName + ":" + server.getPort()); + } + + @Test public void emptyResponse() throws IOException { + server.enqueue(new MockResponse()); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/foo")); + assertEquals(-1, connection.getInputStream().read()); + } + + @Test public void post() throws Exception { + MockResponse response = new MockResponse().setBody("ABCDE"); + server.enqueue(response); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/foo")); + connection.setDoOutput(true); + connection.getOutputStream().write("FGHIJ".getBytes(Util.UTF_8)); + assertContent("ABCDE", connection, Integer.MAX_VALUE); + + RecordedRequest request = server.takeRequest(); + assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); + assertEquals("FGHIJ", request.getUtf8Body()); + } + + @Test public void spdyConnectionReuse() throws Exception { + server.enqueue(new MockResponse().setBody("ABCDEF")); + server.enqueue(new MockResponse().setBody("GHIJKL")); + server.play(); + + HttpURLConnection connection1 = client.open(server.getUrl("/r1")); + HttpURLConnection connection2 = client.open(server.getUrl("/r2")); + assertEquals("ABC", readAscii(connection1.getInputStream(), 3)); + assertEquals("GHI", readAscii(connection2.getInputStream(), 3)); + assertEquals("DEF", readAscii(connection1.getInputStream(), 3)); + assertEquals("JKL", readAscii(connection2.getInputStream(), 3)); + assertEquals(0, server.takeRequest().getSequenceNumber()); + assertEquals(0, server.takeRequest().getSequenceNumber()); + } + + @Test public void gzippedResponseBody() throws Exception { + server.enqueue(new MockResponse().addHeader("Content-Encoding: gzip") + .setBody(gzip("ABCABCABC".getBytes(Util.UTF_8)))); + server.play(); + assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE); + } + + @Test public void authenticate() throws Exception { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED) + .addHeader("www-authenticate: Basic realm=\"protected area\"") + .setBody("Please authenticate.")); + server.enqueue(new MockResponse().setBody("Successful auth!")); + server.play(); + + Authenticator.setDefault(new RecordingAuthenticator()); + HttpURLConnection connection = client.open(server.getUrl("/")); + assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); + + RecordedRequest denied = server.takeRequest(); + assertContainsNoneMatching(denied.getHeaders(), "authorization: Basic .*"); + RecordedRequest accepted = server.takeRequest(); + assertEquals("GET / HTTP/1.1", accepted.getRequestLine()); + assertContains(accepted.getHeaders(), + "authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS); + } + + @Test public void redirect() throws Exception { + server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) + .addHeader("Location: /foo") + .setBody("This page has moved!")); + server.enqueue(new MockResponse().setBody("This is the new location!")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + assertContent("This is the new location!", connection, Integer.MAX_VALUE); + + RecordedRequest request1 = server.takeRequest(); + assertEquals("/", request1.getPath()); + RecordedRequest request2 = server.takeRequest(); + assertEquals("/foo", request2.getPath()); + } + + @Test public void readAfterLastByte() throws Exception { + server.enqueue(new MockResponse().setBody("ABC")); + server.play(); + + HttpURLConnection connection = client.open(server.getUrl("/")); + InputStream in = connection.getInputStream(); + assertEquals("ABC", readAscii(in, 3)); + assertEquals(-1, in.read()); + assertEquals(-1, in.read()); + } + + @Test public void responsesAreCached() throws IOException { + client.setResponseCache(cache); + + server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("A")); + server.play(); + + assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); + assertEquals(1, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(0, cache.getHitCount()); + assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); + assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); + assertEquals(3, cache.getRequestCount()); + assertEquals(1, cache.getNetworkCount()); + assertEquals(2, cache.getHitCount()); + } + + @Test public void acceptAndTransmitCookies() throws Exception { + CookieManager cookieManager = new CookieManager(); + client.setCookieHandler(cookieManager); + server.enqueue( + new MockResponse().addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain()) + .setBody("A")); + server.enqueue(new MockResponse().setBody("B")); + server.play(); + + URL url = server.getUrl("/"); + assertContent("A", client.open(url), Integer.MAX_VALUE); + Map> requestHeaders = Collections.emptyMap(); + assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")), + cookieManager.get(url.toURI(), requestHeaders)); + + assertContent("B", client.open(url), Integer.MAX_VALUE); + RecordedRequest requestA = server.takeRequest(); + assertContainsNoneMatching(requestA.getHeaders(), "Cookie.*"); + RecordedRequest requestB = server.takeRequest(); + assertContains(requestB.getHeaders(), "cookie: c=oreo"); + } + + private void assertContains(Collection collection, T value) { + assertTrue(collection.toString(), collection.contains(value)); + } + + private void assertContent(String expected, URLConnection connection, int limit) + throws IOException { + connection.connect(); + assertEquals(expected, readAscii(connection.getInputStream(), limit)); + ((HttpURLConnection) connection).disconnect(); + } + + private void assertContainsNoneMatching(List headers, String pattern) { + for (String header : headers) { + if (header.matches(pattern)) { + fail("Header " + header + " matches " + pattern); + } } + } - @Test public void get() throws Exception { - MockResponse response = new MockResponse().setBody("ABCDE"); - server.enqueue(response); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/foo")); - assertContent("ABCDE", connection, Integer.MAX_VALUE); - - RecordedRequest request = server.takeRequest(); - assertEquals("GET /foo HTTP/1.1", request.getRequestLine()); - assertContains(request.getHeaders(), ":scheme: https"); - assertContains(request.getHeaders(), ":host: " + hostName + ":" + server.getPort()); + private String readAscii(InputStream in, int count) throws IOException { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < count; i++) { + int value = in.read(); + if (value == -1) { + in.close(); + break; + } + result.append((char) value); } + return result.toString(); + } - @Test public void emptyResponse() throws IOException { - server.enqueue(new MockResponse()); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/foo")); - assertEquals(-1, connection.getInputStream().read()); - } - - @Test public void post() throws Exception { - MockResponse response = new MockResponse().setBody("ABCDE"); - server.enqueue(response); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/foo")); - connection.setDoOutput(true); - connection.getOutputStream().write("FGHIJ".getBytes(Util.UTF_8)); - assertContent("ABCDE", connection, Integer.MAX_VALUE); - - RecordedRequest request = server.takeRequest(); - assertEquals("POST /foo HTTP/1.1", request.getRequestLine()); - assertEquals("FGHIJ", request.getUtf8Body()); - } - - @Test public void spdyConnectionReuse() throws Exception { - server.enqueue(new MockResponse().setBody("ABCDEF")); - server.enqueue(new MockResponse().setBody("GHIJKL")); - server.play(); - - HttpURLConnection connection1 = client.open(server.getUrl("/r1")); - HttpURLConnection connection2 = client.open(server.getUrl("/r2")); - assertEquals("ABC", readAscii(connection1.getInputStream(), 3)); - assertEquals("GHI", readAscii(connection2.getInputStream(), 3)); - assertEquals("DEF", readAscii(connection1.getInputStream(), 3)); - assertEquals("JKL", readAscii(connection2.getInputStream(), 3)); - assertEquals(0, server.takeRequest().getSequenceNumber()); - assertEquals(0, server.takeRequest().getSequenceNumber()); - } - - @Test public void gzippedResponseBody() throws Exception { - server.enqueue(new MockResponse() - .addHeader("Content-Encoding: gzip") - .setBody(gzip("ABCABCABC".getBytes(Util.UTF_8)))); - server.play(); - assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE); - } - - @Test public void authenticate() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_UNAUTHORIZED) - .addHeader("www-authenticate: Basic realm=\"protected area\"") - .setBody("Please authenticate.")); - server.enqueue(new MockResponse().setBody("Successful auth!")); - server.play(); - - Authenticator.setDefault(new RecordingAuthenticator()); - HttpURLConnection connection = client.open(server.getUrl("/")); - assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE)); - - RecordedRequest denied = server.takeRequest(); - assertContainsNoneMatching(denied.getHeaders(), "authorization: Basic .*"); - RecordedRequest accepted = server.takeRequest(); - assertEquals("GET / HTTP/1.1", accepted.getRequestLine()); - assertContains(accepted.getHeaders(), "authorization: Basic " - + RecordingAuthenticator.BASE_64_CREDENTIALS); - } - - @Test public void redirect() throws Exception { - server.enqueue(new MockResponse() - .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP) - .addHeader("Location: /foo") - .setBody("This page has moved!")); - server.enqueue(new MockResponse().setBody("This is the new location!")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - assertContent("This is the new location!", connection, Integer.MAX_VALUE); - - RecordedRequest request1 = server.takeRequest(); - assertEquals("/", request1.getPath()); - RecordedRequest request2 = server.takeRequest(); - assertEquals("/foo", request2.getPath()); - } - - @Test public void readAfterLastByte() throws Exception { - server.enqueue(new MockResponse().setBody("ABC")); - server.play(); - - HttpURLConnection connection = client.open(server.getUrl("/")); - InputStream in = connection.getInputStream(); - assertEquals("ABC", readAscii(in, 3)); - assertEquals(-1, in.read()); - assertEquals(-1, in.read()); - } - - @Test public void responsesAreCached() throws IOException { - client.setResponseCache(cache); - - server.enqueue(new MockResponse() - .addHeader("cache-control: max-age=60") - .setBody("A")); - server.play(); - - assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); - assertEquals(1, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(0, cache.getHitCount()); - assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); - assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE); - assertEquals(3, cache.getRequestCount()); - assertEquals(1, cache.getNetworkCount()); - assertEquals(2, cache.getHitCount()); - } - - @Test public void acceptAndTransmitCookies() throws Exception { - CookieManager cookieManager = new CookieManager(); - client.setCookieHandler(cookieManager); - server.enqueue(new MockResponse() - .addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain()) - .setBody("A")); - server.enqueue(new MockResponse().setBody("B")); - server.play(); - - URL url = server.getUrl("/"); - assertContent("A", client.open(url), Integer.MAX_VALUE); - Map> requestHeaders = Collections.emptyMap(); - assertEquals(Collections.singletonMap("Cookie", Arrays.asList("c=oreo")), - cookieManager.get(url.toURI(), requestHeaders)); - - assertContent("B", client.open(url), Integer.MAX_VALUE); - RecordedRequest requestA = server.takeRequest(); - assertContainsNoneMatching(requestA.getHeaders(), "Cookie.*"); - RecordedRequest requestB = server.takeRequest(); - assertContains(requestB.getHeaders(), "cookie: c=oreo"); - } - - private void assertContains(Collection collection, T value) { - assertTrue(collection.toString(), collection.contains(value)); - } - - private void assertContent(String expected, URLConnection connection, int limit) - throws IOException { - connection.connect(); - assertEquals(expected, readAscii(connection.getInputStream(), limit)); - ((HttpURLConnection) connection).disconnect(); - } - - private void assertContainsNoneMatching(List headers, String pattern) { - for (String header : headers) { - if (header.matches(pattern)) { - fail("Header " + header + " matches " + pattern); - } - } - } - - private String readAscii(InputStream in, int count) throws IOException { - StringBuilder result = new StringBuilder(); - for (int i = 0; i < count; i++) { - int value = in.read(); - if (value == -1) { - in.close(); - break; - } - result.append((char) value); - } - return result.toString(); - } - - public byte[] gzip(byte[] bytes) throws IOException { - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - OutputStream gzippedOut = new GZIPOutputStream(bytesOut); - gzippedOut.write(bytes); - gzippedOut.close(); - return bytesOut.toByteArray(); - } + public byte[] gzip(byte[] bytes) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + OutputStream gzippedOut = new GZIPOutputStream(bytesOut); + gzippedOut.write(bytes); + gzippedOut.close(); + return bytesOut.toByteArray(); + } } 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 23e4bf579..54e058bcd 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java @@ -32,233 +32,231 @@ import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; -/** - * Replays prerecorded outgoing frames and records incoming frames. - */ +/** Replays prerecorded outgoing frames and records incoming frames. */ 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 List outFrames = new ArrayList(); - private final BlockingQueue inFrames = new LinkedBlockingQueue(); - private int port; - private final Executor executor = Executors.newCachedThreadPool( - Util.newThreadFactory("MockSpdyPeer", true)); - private ServerSocket serverSocket; - private Socket socket; + private int frameCount = 0; + private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut); + private final List outFrames = new ArrayList(); + private final BlockingQueue inFrames = new LinkedBlockingQueue(); + private int port; + private final Executor executor = + Executors.newCachedThreadPool(Util.newThreadFactory("MockSpdyPeer", true)); + private ServerSocket serverSocket; + private Socket socket; - public void acceptFrame() { - frameCount++; + public void acceptFrame() { + frameCount++; + } + + public SpdyWriter sendFrame() { + outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE)); + return spdyWriter; + } + + /** + * Sends a frame, truncated to {@code truncateToLength} bytes. This is only + * useful for testing error handling as the truncated frame will be + * malformed. + */ + public SpdyWriter sendTruncatedFrame(int truncateToLength) { + outFrames.add(new OutFrame(frameCount++, bytesOut.size(), truncateToLength)); + return spdyWriter; + } + + public int getPort() { + return port; + } + + public InFrame takeFrame() throws InterruptedException { + return inFrames.take(); + } + + public void play() throws IOException { + 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(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + } + + 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); + + Iterator outFramesIterator = outFrames.iterator(); + byte[] outBytes = bytesOut.toByteArray(); + OutFrame nextOutFrame = null; + + for (int i = 0; i < frameCount; i++) { + if (nextOutFrame == null && outFramesIterator.hasNext()) { + nextOutFrame = outFramesIterator.next(); + } + + if (nextOutFrame != null && nextOutFrame.sequence == i) { + int start = nextOutFrame.start; + int truncateToLength = nextOutFrame.truncateToLength; + int end; + if (outFramesIterator.hasNext()) { + nextOutFrame = outFramesIterator.next(); + end = nextOutFrame.start; + } else { + end = outBytes.length; + } + + // write a frame + int length = Math.min(end - start, truncateToLength); + out.write(outBytes, start, length); + } else { + // read a frame + InFrame inFrame = new InFrame(i, reader); + reader.nextFrame(inFrame); + inFrames.add(inFrame); + } + } + Util.closeQuietly(socket); + } + + public Socket openSocket() throws IOException { + 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; + private final int truncateToLength; + + private OutFrame(int sequence, int start, int truncateToLength) { + this.sequence = sequence; + this.start = start; + this.truncateToLength = truncateToLength; + } + } + + public static class InFrame implements SpdyReader.Handler { + public final int sequence; + public final SpdyReader reader; + public int type = -1; + public int flags; + 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; + + public InFrame(int sequence, SpdyReader reader) { + this.sequence = sequence; + this.reader = reader; } - public SpdyWriter sendFrame() { - outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE)); - return spdyWriter; + @Override public void settings(int flags, Settings settings) { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_SETTINGS; + this.flags = flags; + this.settings = settings; } - /** - * Sends a frame, truncated to {@code truncateToLength} bytes. This is only - * useful for testing error handling as the truncated frame will be - * malformed. - */ - public SpdyWriter sendTruncatedFrame(int truncateToLength) { - outFrames.add(new OutFrame(frameCount++, bytesOut.size(), truncateToLength)); - return spdyWriter; + @Override + public void synStream(int flags, int streamId, int associatedStreamId, 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; } - public int getPort() { - return port; + @Override public void synReply(int flags, int streamId, List nameValueBlock) { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_SYN_REPLY; + this.streamId = streamId; + this.flags = flags; + this.nameValueBlock = nameValueBlock; } - public InFrame takeFrame() throws InterruptedException { - return inFrames.take(); + @Override public void headers(int flags, int streamId, List nameValueBlock) { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_HEADERS; + this.streamId = streamId; + this.flags = flags; + this.nameValueBlock = nameValueBlock; } - public void play() throws IOException { - 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(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); + @Override public void data(int flags, int streamId, InputStream in, int length) + throws IOException { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_DATA; + this.flags = flags; + this.streamId = streamId; + this.data = new byte[length]; + Util.readFully(in, this.data); } - 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); - - Iterator outFramesIterator = outFrames.iterator(); - byte[] outBytes = bytesOut.toByteArray(); - OutFrame nextOutFrame = null; - - for (int i = 0; i < frameCount; i++) { - if (nextOutFrame == null && outFramesIterator.hasNext()) { - nextOutFrame = outFramesIterator.next(); - } - - if (nextOutFrame != null && nextOutFrame.sequence == i) { - int start = nextOutFrame.start; - int truncateToLength = nextOutFrame.truncateToLength; - int end; - if (outFramesIterator.hasNext()) { - nextOutFrame = outFramesIterator.next(); - end = nextOutFrame.start; - } else { - end = outBytes.length; - } - - // write a frame - int length = Math.min(end - start, truncateToLength); - out.write(outBytes, start, length); - - } else { - // read a frame - InFrame inFrame = new InFrame(i, reader); - reader.nextFrame(inFrame); - inFrames.add(inFrame); - } - } - Util.closeQuietly(socket); + @Override public void rstStream(int flags, int streamId, int statusCode) { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_RST_STREAM; + this.flags = flags; + this.streamId = streamId; + this.statusCode = statusCode; } - public Socket openSocket() throws IOException { - return new Socket("localhost", port); + @Override public void ping(int flags, int streamId) { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_PING; + this.flags = flags; + this.streamId = streamId; } - @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; - } + @Override public void noop() { + if (this.type != -1) throw new IllegalStateException(); + this.type = SpdyConnection.TYPE_NOOP; } - private static class OutFrame { - private final int sequence; - private final int start; - private final int truncateToLength; - - private OutFrame(int sequence, int start, int truncateToLength) { - this.sequence = sequence; - this.start = start; - this.truncateToLength = truncateToLength; - } + @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; } - public static class InFrame implements SpdyReader.Handler { - public final int sequence; - public final SpdyReader reader; - public int type = -1; - public int flags; - 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; - - public InFrame(int sequence, SpdyReader reader) { - this.sequence = sequence; - this.reader = reader; - } - - @Override public void settings(int flags, Settings settings) { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_SETTINGS; - this.flags = flags; - this.settings = settings; - } - - @Override public void synStream(int flags, int streamId, int associatedStreamId, - 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; - } - - @Override public void synReply(int flags, int streamId, List nameValueBlock) { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_SYN_REPLY; - this.streamId = streamId; - this.flags = flags; - this.nameValueBlock = nameValueBlock; - } - - @Override public void headers(int flags, int streamId, List nameValueBlock) { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_HEADERS; - this.streamId = streamId; - this.flags = flags; - this.nameValueBlock = nameValueBlock; - } - - @Override public void data(int flags, int streamId, InputStream in, int length) - throws IOException { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_DATA; - this.flags = flags; - this.streamId = streamId; - this.data = new byte[length]; - Util.readFully(in, this.data); - } - - @Override public void rstStream(int flags, int streamId, int statusCode) { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_RST_STREAM; - this.flags = flags; - this.streamId = streamId; - this.statusCode = statusCode; - } - - @Override public void ping(int flags, int streamId) { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_PING; - this.flags = flags; - this.streamId = streamId; - } - - @Override public void noop() { - if (this.type != -1) throw new IllegalStateException(); - this.type = SpdyConnection.TYPE_NOOP; - } - - @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; - } + @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 0bbe5f9f1..a906fc7cb 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java @@ -15,6 +15,8 @@ */ package com.squareup.okhttp.internal.spdy; +import org.junit.Test; + 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; @@ -24,131 +26,130 @@ import static com.squareup.okhttp.internal.spdy.Settings.UPLOAD_BANDWIDTH; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import org.junit.Test; public final class SettingsTest { - @Test public void unsetField() { - Settings settings = new Settings(); - assertEquals(-3, settings.getUploadBandwidth(-3)); - } + @Test public void unsetField() { + Settings settings = new Settings(); + assertEquals(-3, settings.getUploadBandwidth(-3)); + } - @Test public void setFields() { - Settings settings = new Settings(); + @Test public void setFields() { + Settings settings = new Settings(); - assertEquals(-3, settings.getUploadBandwidth(-3)); - settings.set(Settings.UPLOAD_BANDWIDTH, 0, 42); - assertEquals(42, settings.getUploadBandwidth(-3)); + assertEquals(-3, settings.getUploadBandwidth(-3)); + settings.set(Settings.UPLOAD_BANDWIDTH, 0, 42); + assertEquals(42, settings.getUploadBandwidth(-3)); - assertEquals(-3, settings.getDownloadBandwidth(-3)); - settings.set(Settings.DOWNLOAD_BANDWIDTH, 0, 53); - assertEquals(53, settings.getDownloadBandwidth(-3)); + assertEquals(-3, settings.getDownloadBandwidth(-3)); + settings.set(Settings.DOWNLOAD_BANDWIDTH, 0, 53); + assertEquals(53, settings.getDownloadBandwidth(-3)); - assertEquals(-3, settings.getRoundTripTime(-3)); - settings.set(Settings.ROUND_TRIP_TIME, 0, 64); - assertEquals(64, settings.getRoundTripTime(-3)); + assertEquals(-3, settings.getRoundTripTime(-3)); + settings.set(Settings.ROUND_TRIP_TIME, 0, 64); + assertEquals(64, settings.getRoundTripTime(-3)); - assertEquals(-3, settings.getMaxConcurrentStreams(-3)); - settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 75); - assertEquals(75, settings.getMaxConcurrentStreams(-3)); + assertEquals(-3, settings.getMaxConcurrentStreams(-3)); + settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 75); + assertEquals(75, settings.getMaxConcurrentStreams(-3)); - assertEquals(-3, settings.getCurrentCwnd(-3)); - settings.set(Settings.CURRENT_CWND, 0, 86); - assertEquals(86, settings.getCurrentCwnd(-3)); + assertEquals(-3, settings.getCurrentCwnd(-3)); + settings.set(Settings.CURRENT_CWND, 0, 86); + assertEquals(86, settings.getCurrentCwnd(-3)); - assertEquals(-3, settings.getDownloadRetransRate(-3)); - settings.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 97); - assertEquals(97, settings.getDownloadRetransRate(-3)); + assertEquals(-3, settings.getDownloadRetransRate(-3)); + settings.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 97); + assertEquals(97, settings.getDownloadRetransRate(-3)); - assertEquals(-3, settings.getInitialWindowSize(-3)); - settings.set(Settings.INITIAL_WINDOW_SIZE, 0, 108); - assertEquals(108, settings.getInitialWindowSize(-3)); + 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)); - } + assertEquals(-3, settings.getClientCertificateVectorSize(-3)); + settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117); + assertEquals(117, settings.getClientCertificateVectorSize(-3)); + } - @Test public void isPersisted() { - Settings settings = new Settings(); + @Test public void isPersisted() { + Settings settings = new Settings(); - // Initially false. - assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + // Initially false. + assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - // Set no flags. - settings.set(Settings.ROUND_TRIP_TIME, 0, 0); - assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + // Set no flags. + settings.set(Settings.ROUND_TRIP_TIME, 0, 0); + assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - // Set the wrong flag. - settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0); - assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + // Set the wrong flag. + settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0); + assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - // Set the right flag. - settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0); - assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + // Set the right flag. + settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0); + assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - // Set multiple flags. - settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0); - assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + // Set multiple flags. + settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0); + assertTrue(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - // Clear the flag. - settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0); - assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + // Clear the flag. + settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0); + assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - // Clear all flags. - settings.set(Settings.ROUND_TRIP_TIME, 0, 0); - assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); - } + // Clear all flags. + settings.set(Settings.ROUND_TRIP_TIME, 0, 0); + assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME)); + } - @Test public void persistValue() { - Settings settings = new Settings(); + @Test public void persistValue() { + Settings settings = new Settings(); - // Initially false. - assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); + // Initially false. + assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); - // Set no flags. - settings.set(Settings.ROUND_TRIP_TIME, 0, 0); - assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); + // Set no flags. + settings.set(Settings.ROUND_TRIP_TIME, 0, 0); + assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); - // Set the wrong flag. - settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0); - assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); + // Set the wrong flag. + settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0); + assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); - // Set the right flag. - settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0); - assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME)); + // Set the right flag. + settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0); + assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME)); - // Set multiple flags. - settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0); - assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME)); + // Set multiple flags. + settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0); + assertTrue(settings.persistValue(Settings.ROUND_TRIP_TIME)); - // Clear the flag. - settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0); - assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); + // Clear the flag. + settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0); + assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); - // Clear all flags. - settings.set(Settings.ROUND_TRIP_TIME, 0, 0); - assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); - } + // Clear all flags. + settings.set(Settings.ROUND_TRIP_TIME, 0, 0); + assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME)); + } - @Test public void merge() { - Settings a = new Settings(); - a.set(UPLOAD_BANDWIDTH, PERSIST_VALUE, 100); - a.set(DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200); - a.set(DOWNLOAD_RETRANS_RATE, 0, 300); + @Test public void merge() { + Settings a = new Settings(); + a.set(UPLOAD_BANDWIDTH, PERSIST_VALUE, 100); + a.set(DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200); + a.set(DOWNLOAD_RETRANS_RATE, 0, 300); - Settings b = new Settings(); - b.set(DOWNLOAD_BANDWIDTH, 0, 400); - b.set(DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500); - b.set(MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600); + Settings b = new Settings(); + b.set(DOWNLOAD_BANDWIDTH, 0, 400); + b.set(DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500); + b.set(MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600); - a.merge(b); - assertEquals(100, a.getUploadBandwidth(-1)); - assertEquals(PERSIST_VALUE, a.flags(UPLOAD_BANDWIDTH)); - assertEquals(400, a.getDownloadBandwidth(-1)); - assertEquals(0, a.flags(DOWNLOAD_BANDWIDTH)); - assertEquals(500, a.getDownloadRetransRate(-1)); - assertEquals(PERSIST_VALUE, a.flags(DOWNLOAD_RETRANS_RATE)); - assertEquals(600, a.getMaxConcurrentStreams(-1)); - assertEquals(PERSIST_VALUE, a.flags(MAX_CONCURRENT_STREAMS)); - } + a.merge(b); + assertEquals(100, a.getUploadBandwidth(-1)); + assertEquals(PERSIST_VALUE, a.flags(UPLOAD_BANDWIDTH)); + assertEquals(400, a.getDownloadBandwidth(-1)); + assertEquals(0, a.flags(DOWNLOAD_BANDWIDTH)); + assertEquals(500, a.getDownloadRetransRate(-1)); + assertEquals(PERSIST_VALUE, a.flags(DOWNLOAD_RETRANS_RATE)); + assertEquals(600, a.getMaxConcurrentStreams(-1)); + assertEquals(PERSIST_VALUE, a.flags(MAX_CONCURRENT_STREAMS)); + } } 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 c8f79b3de..7dd23f620 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java @@ -17,6 +17,17 @@ package com.squareup.okhttp.internal.spdy; import com.squareup.okhttp.internal.Util; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.After; +import org.junit.Test; + 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; @@ -37,1013 +48,985 @@ 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 static com.squareup.okhttp.internal.spdy.SpdyStream.WINDOW_UPDATE_THRESHOLD; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InterruptedIOException; -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; -import org.junit.Test; public final class SpdyConnectionTest { - private static final IncomingStreamHandler REJECT_INCOMING_STREAMS - = new IncomingStreamHandler() { - @Override public void receive(SpdyStream stream) throws IOException { - throw new AssertionError(); - } + private static final IncomingStreamHandler REJECT_INCOMING_STREAMS = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + throw new AssertionError(); + } + }; + 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(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8")); + peer.acceptFrame(); // DATA + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); + assertStreamData("robot", stream.getInputStream()); + writeAndClose(stream, "c3po"); + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + assertEquals(0, synStream.flags); + assertEquals(1, synStream.streamId); + assertEquals(0, synStream.associatedStreamId); + assertEquals(Arrays.asList("b", "banana"), synStream.nameValueBlock); + MockSpdyPeer.InFrame requestData = peer.takeFrame(); + assertTrue(Arrays.equals("c3po".getBytes("UTF-8"), requestData.data)); + } + + @Test public void headersOnlyStreamIsClosedAfterReplyHeaders() throws Exception { + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); + peer.play(); + + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, false); + assertEquals(1, connection.openStreamCount()); + assertEquals(Arrays.asList("b", "banana"), stream.getResponseHeaders()); + assertEquals(0, connection.openStreamCount()); + } + + @Test public void clientCreatesStreamAndServerRepliesWithFin() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.acceptFrame(); // PING + peer.sendFrame().synReply(FLAG_FIN, 1, Arrays.asList("a", "android")); + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + connection.newStream(Arrays.asList("b", "banana"), false, true); + assertEquals(1, connection.openStreamCount()); + connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received. + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + } + + @Test public void serverCreatesStreamAndClientReplies() throws Exception { + // write the mocking script + peer.sendFrame().synStream(0, 2, 0, 5, 129, Arrays.asList("a", "android")); + peer.acceptFrame(); // SYN_REPLY + peer.play(); + + // play it back + final AtomicInteger receiveCount = new AtomicInteger(); + IncomingStreamHandler handler = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + 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); + } }; - private final MockSpdyPeer peer = new MockSpdyPeer(); + new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build(); - @After public void tearDown() throws Exception { - peer.close(); + // verify the peer received what was expected + MockSpdyPeer.InFrame reply = peer.takeFrame(); + assertEquals(TYPE_SYN_REPLY, reply.type); + assertEquals(0, reply.flags); + assertEquals(2, reply.streamId); + assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock); + assertEquals(1, receiveCount.get()); + } + + @Test public void replyWithNoData() throws Exception { + // write the mocking script + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android")); + peer.acceptFrame(); // SYN_REPLY + peer.play(); + + // play it back + final AtomicInteger receiveCount = new AtomicInteger(); + IncomingStreamHandler handler = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + stream.reply(Arrays.asList("b", "banana"), false); + receiveCount.incrementAndGet(); + } + }; + new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame reply = peer.takeFrame(); + assertEquals(TYPE_SYN_REPLY, reply.type); + assertEquals(FLAG_FIN, reply.flags); + assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock); + assertEquals(1, receiveCount.get()); + } + + @Test public void noop() throws Exception { + // write the mocking script + peer.acceptFrame(); // NOOP + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + connection.noop(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_NOOP, ping.type); + assertEquals(0, ping.flags); + } + + @Test public void serverPingsClient() throws Exception { + // write the mocking script + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); // PING + peer.play(); + + // play it back + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + assertEquals(0, ping.flags); + assertEquals(2, ping.streamId); + } + + @Test public void clientPingsServer() throws Exception { + // write the mocking script + peer.acceptFrame(); // PING + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + Ping ping = connection.ping(); + assertTrue(ping.roundTripTime() > 0); + assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1)); + + // verify the peer received what was expected + MockSpdyPeer.InFrame pingFrame = peer.takeFrame(); + assertEquals(TYPE_PING, pingFrame.type); + assertEquals(0, pingFrame.flags); + assertEquals(1, pingFrame.streamId); + } + + @Test public void unexpectedPingIsNotReturned() throws Exception { + // write the mocking script + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); // PING + peer.sendFrame().ping(0, 3); // This ping will not be returned. + peer.sendFrame().ping(0, 4); + peer.acceptFrame(); // PING + peer.play(); + + // play it back + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame ping2 = peer.takeFrame(); + assertEquals(2, ping2.streamId); + MockSpdyPeer.InFrame ping4 = peer.takeFrame(); + assertEquals(4, ping4.streamId); + } + + @Test public void serverSendsSettingsToClient() throws Exception { + // write the mocking script + Settings settings = new Settings(); + settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10); + peer.sendFrame().settings(Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS, settings); + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); // PING + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + + peer.takeFrame(); // Guarantees that the Settings frame has been processed. + synchronized (connection) { + assertEquals(10, connection.settings.getMaxConcurrentStreams(-1)); + } + } + + @Test public void multipleSettingsFramesAreMerged() throws Exception { + // write the mocking script + Settings settings1 = new Settings(); + settings1.set(Settings.UPLOAD_BANDWIDTH, PERSIST_VALUE, 100); + settings1.set(Settings.DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200); + settings1.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 300); + peer.sendFrame().settings(0, settings1); + Settings settings2 = new Settings(); + settings2.set(Settings.DOWNLOAD_BANDWIDTH, 0, 400); + settings2.set(Settings.DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500); + settings2.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600); + peer.sendFrame().settings(0, settings2); + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + + peer.takeFrame(); // Guarantees that the Settings frame has been processed. + synchronized (connection) { + assertEquals(100, connection.settings.getUploadBandwidth(-1)); + assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.UPLOAD_BANDWIDTH)); + assertEquals(400, connection.settings.getDownloadBandwidth(-1)); + assertEquals(0, connection.settings.flags(Settings.DOWNLOAD_BANDWIDTH)); + assertEquals(500, connection.settings.getDownloadRetransRate(-1)); + assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.DOWNLOAD_RETRANS_RATE)); + assertEquals(600, connection.settings.getMaxConcurrentStreams(-1)); + assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.MAX_CONCURRENT_STREAMS)); + } + } + + @Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception { + // write the mocking script + peer.sendFrame().data(SpdyConnection.FLAG_FIN, 42, "bogus".getBytes("UTF-8")); + peer.acceptFrame(); // RST_STREAM + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); // PING + peer.play(); + + // play it back + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(0, rstStream.flags); + assertEquals(42, rstStream.streamId); + assertEquals(RST_INVALID_STREAM, rstStream.statusCode); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(2, ping.streamId); + } + + @Test public void bogusReplyFrameDoesNotDisruptConnection() throws Exception { + // write the mocking script + peer.sendFrame().synReply(0, 42, Arrays.asList("a", "android")); + peer.acceptFrame(); // RST_STREAM + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); // PING + peer.play(); + + // play it back + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS).build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(0, rstStream.flags); + assertEquals(42, rstStream.streamId); + assertEquals(RST_INVALID_STREAM, rstStream.statusCode); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(2, ping.streamId); + } + + @Test public void clientClosesClientOutputStream() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); + peer.acceptFrame(); // TYPE_DATA + peer.acceptFrame(); // TYPE_DATA with FLAG_FIN + peer.acceptFrame(); // PING + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, false); + OutputStream out = stream.getOutputStream(); + out.write("square".getBytes(UTF_8)); + out.flush(); + assertEquals(1, connection.openStreamCount()); + out.close(); + try { + out.write("round".getBytes(UTF_8)); + fail(); + } catch (Exception expected) { + assertEquals("stream closed", expected.getMessage()); + } + connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received. + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + assertEquals(FLAG_UNIDIRECTIONAL, synStream.flags); + MockSpdyPeer.InFrame data = peer.takeFrame(); + assertEquals(TYPE_DATA, data.type); + assertEquals(0, data.flags); + assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data)); + MockSpdyPeer.InFrame fin = peer.takeFrame(); + assertEquals(TYPE_DATA, fin.type); + assertEquals(FLAG_FIN, fin.flags); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + assertEquals(1, ping.streamId); + } + + @Test public void serverClosesClientOutputStream() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().rstStream(1, SpdyStream.RST_CANCEL); + peer.acceptFrame(); // PING + peer.sendFrame().ping(0, 1); + peer.acceptFrame(); // DATA + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); + OutputStream out = stream.getOutputStream(); + connection.ping().roundTripTime(); // Ensure that the RST_CANCEL has been received. + try { + out.write("square".getBytes(UTF_8)); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: CANCEL", expected.getMessage()); + } + out.close(); + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + assertEquals(0, synStream.flags); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + assertEquals(1, ping.streamId); + MockSpdyPeer.InFrame data = peer.takeFrame(); + assertEquals(TYPE_DATA, data.type); + assertEquals(1, data.streamId); + assertEquals(FLAG_FIN, data.flags); + } + + /** + * Test that the client sends a RST_STREAM if doing so won't disrupt the + * output stream. + */ + @Test public void clientClosesClientInputStream() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.acceptFrame(); // RST_STREAM + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, true); + InputStream in = stream.getInputStream(); + OutputStream out = stream.getOutputStream(); + in.close(); + try { + in.read(); + fail(); + } catch (IOException expected) { + assertEquals("stream closed", expected.getMessage()); + } + try { + out.write('a'); + fail(); + } catch (IOException expected) { + assertEquals("stream finished", expected.getMessage()); + } + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + assertEquals(SpdyConnection.FLAG_FIN, synStream.flags); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode); + } + + /** + * Test that the client doesn't send a RST_STREAM if doing so will disrupt + * the output stream. + */ + @Test public void clientClosesClientInputStreamIfOutputStreamIsClosed() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.acceptFrame(); // DATA + peer.acceptFrame(); // DATA with FLAG_FIN + peer.acceptFrame(); // RST_STREAM + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); + InputStream in = stream.getInputStream(); + OutputStream out = stream.getOutputStream(); + in.close(); + try { + in.read(); + fail(); + } catch (IOException expected) { + assertEquals("stream closed", expected.getMessage()); + } + out.write("square".getBytes(UTF_8)); + out.flush(); + out.close(); + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + assertEquals(0, synStream.flags); + MockSpdyPeer.InFrame data = peer.takeFrame(); + assertEquals(TYPE_DATA, data.type); + assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data)); + MockSpdyPeer.InFrame fin = peer.takeFrame(); + assertEquals(TYPE_DATA, fin.type); + assertEquals(FLAG_FIN, fin.flags); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode); + } + + @Test public void serverClosesClientInputStream() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); + peer.sendFrame().data(FLAG_FIN, 1, "square".getBytes(UTF_8)); + peer.play(); + + // play it back + SpdyConnection connection = + new SpdyConnection.Builder(true, peer.openSocket()).handler(REJECT_INCOMING_STREAMS) + .build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, true); + InputStream in = stream.getInputStream(); + assertStreamData("square", in); + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + assertEquals(SpdyConnection.FLAG_FIN, synStream.flags); + } + + @Test public void remoteDoubleSynReply() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + peer.acceptFrame(); // PING + peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); + peer.sendFrame().ping(0, 1); + peer.acceptFrame(); // RST_STREAM + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("c", "cola"), true, true); + assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); + connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received. + try { + stream.getInputStream().read(); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: STREAM_IN_USE", expected.getMessage()); } - @Test public void clientCreatesStreamAndServerReplies() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8")); - peer.acceptFrame(); // DATA - peer.play(); + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(1, rstStream.streamId); + assertEquals(0, rstStream.flags); + assertEquals(RST_STREAM_IN_USE, rstStream.statusCode); + } - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); - assertStreamData("robot", stream.getInputStream()); - writeAndClose(stream, "c3po"); - assertEquals(0, connection.openStreamCount()); + @Test public void remoteDoubleSynStream() throws Exception { + // write the mocking script + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android")); + peer.acceptFrame(); // SYN_REPLY + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "banana")); + peer.acceptFrame(); // RST_STREAM + peer.play(); - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - assertEquals(0, synStream.flags); - assertEquals(1, synStream.streamId); - assertEquals(0, synStream.associatedStreamId); - assertEquals(Arrays.asList("b", "banana"), synStream.nameValueBlock); - MockSpdyPeer.InFrame requestData = peer.takeFrame(); - assertTrue(Arrays.equals("c3po".getBytes("UTF-8"), requestData.data)); + // play it back + final AtomicInteger receiveCount = new AtomicInteger(); + IncomingStreamHandler handler = new IncomingStreamHandler() { + @Override public void receive(SpdyStream stream) throws IOException { + receiveCount.incrementAndGet(); + assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders()); + assertEquals(-1, stream.getRstStatusCode()); + stream.reply(Arrays.asList("c", "cola"), true); + } + }; + new SpdyConnection.Builder(true, peer.openSocket()).handler(handler).build(); + + // verify the peer received what was expected + MockSpdyPeer.InFrame reply = peer.takeFrame(); + assertEquals(TYPE_SYN_REPLY, reply.type); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(2, rstStream.streamId); + assertEquals(0, rstStream.flags); + assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode); + assertEquals(1, receiveCount.intValue()); + } + + @Test public void remoteSendsDataAfterInFinished() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8")); + peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "c3po".getBytes("UTF-8")); // Ignored. + peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded. + peer.acceptFrame(); // PING + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); + assertStreamData("robot", stream.getInputStream()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + assertEquals(2, ping.streamId); + assertEquals(0, ping.flags); + } + + @Test public void remoteSendsTooMuchData() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); + peer.sendFrame().data(0, 1, new byte[64 * 1024 + 1]); + peer.acceptFrame(); // RST_STREAM + peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded. + peer.acceptFrame(); // PING + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); + assertEquals(Arrays.asList("b", "banana"), stream.getResponseHeaders()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(1, rstStream.streamId); + assertEquals(0, rstStream.flags); + assertEquals(RST_FLOW_CONTROL_ERROR, rstStream.statusCode); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + assertEquals(2, ping.streamId); + } + + @Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().rstStream(1, RST_REFUSED_STREAM); + peer.sendFrame().ping(0, 2); + peer.acceptFrame(); // PING + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); + try { + stream.getResponseHeaders(); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage()); + } + assertEquals(0, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + assertEquals(2, ping.streamId); + assertEquals(0, ping.flags); + } + + @Test public void receiveGoAway() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM 1 + peer.acceptFrame(); // SYN_STREAM 3 + peer.sendFrame().goAway(0, 1, GOAWAY_PROTOCOL_ERROR); + peer.acceptFrame(); // PING + peer.sendFrame().ping(0, 1); + peer.acceptFrame(); // DATA STREAM 1 + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream1 = connection.newStream(Arrays.asList("a", "android"), true, true); + SpdyStream stream2 = connection.newStream(Arrays.asList("b", "banana"), true, true); + connection.ping().roundTripTime(); // Ensure that the GO_AWAY has been received. + stream1.getOutputStream().write("abc".getBytes(UTF_8)); + try { + stream2.getOutputStream().write("abc".getBytes(UTF_8)); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage()); + } + stream1.getOutputStream().write("def".getBytes(UTF_8)); + stream1.getOutputStream().close(); + try { + connection.newStream(Arrays.asList("c", "cola"), true, true); + fail(); + } catch (IOException expected) { + assertEquals("shutdown", expected.getMessage()); + } + assertEquals(1, connection.openStreamCount()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream1 = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream1.type); + MockSpdyPeer.InFrame synStream2 = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream2.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + MockSpdyPeer.InFrame data1 = peer.takeFrame(); + assertEquals(TYPE_DATA, data1.type); + assertEquals(1, data1.streamId); + assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data)); + } + + @Test public void sendGoAway() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM 1 + peer.acceptFrame(); // GOAWAY + peer.acceptFrame(); // PING + peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "b")); // Should be ignored! + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + connection.newStream(Arrays.asList("a", "android"), true, true); + Ping ping = connection.ping(); + connection.shutdown(GOAWAY_PROTOCOL_ERROR); + assertEquals(1, connection.openStreamCount()); + ping.roundTripTime(); // Prevent the peer from exiting prematurely. + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream1 = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream1.type); + MockSpdyPeer.InFrame pingFrame = peer.takeFrame(); + assertEquals(TYPE_PING, pingFrame.type); + 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 { + // write the mocking script + peer.acceptFrame(); // GOAWAY + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + connection.shutdown(GOAWAY_INTERNAL_ERROR); + try { + connection.ping(); + fail(); + } catch (IOException expected) { + assertEquals("shutdown", expected.getMessage()); } - @Test public void headersOnlyStreamIsClosedAfterReplyHeaders() throws Exception { - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); - peer.play(); + // verify the peer received what was expected + MockSpdyPeer.InFrame goaway = peer.takeFrame(); + assertEquals(TYPE_GOAWAY, goaway.type); + assertEquals(GOAWAY_INTERNAL_ERROR, goaway.statusCode); + } - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, false); - assertEquals(1, connection.openStreamCount()); - assertEquals(Arrays.asList("b", "banana"), stream.getResponseHeaders()); - assertEquals(0, connection.openStreamCount()); + @Test public void close() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.acceptFrame(); // GOAWAY + peer.acceptFrame(); // RST_STREAM + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); + assertEquals(1, connection.openStreamCount()); + connection.close(); + assertEquals(0, connection.openStreamCount()); + try { + connection.newStream(Arrays.asList("b", "banana"), true, true); + fail(); + } catch (IOException expected) { + assertEquals("shutdown", expected.getMessage()); + } + try { + stream.getOutputStream().write(0); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: CANCEL", expected.getMessage()); + } + try { + stream.getInputStream().read(); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: CANCEL", expected.getMessage()); } - @Test public void clientCreatesStreamAndServerRepliesWithFin() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.acceptFrame(); // PING - peer.sendFrame().synReply(FLAG_FIN, 1, Arrays.asList("a", "android")); - peer.sendFrame().ping(0, 1); - peer.play(); + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame goaway = peer.takeFrame(); + assertEquals(TYPE_GOAWAY, goaway.type); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(1, rstStream.streamId); + } - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - connection.newStream(Arrays.asList("b", "banana"), false, true); - assertEquals(1, connection.openStreamCount()); - connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received. - assertEquals(0, connection.openStreamCount()); + @Test public void closeCancelsPings() throws Exception { + // write the mocking script + peer.acceptFrame(); // PING + peer.acceptFrame(); // GOAWAY + peer.play(); - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + Ping ping = connection.ping(); + connection.close(); + assertEquals(-1, ping.roundTripTime()); + } + + @Test public void readTimeoutExpires() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + peer.acceptFrame(); // PING + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + stream.setReadTimeout(1000); + InputStream in = stream.getInputStream(); + long startNanos = System.nanoTime(); + try { + in.read(); + fail(); + } catch (IOException expected) { + } + long elapsedNanos = System.nanoTime() - startNanos; + assertEquals(1000d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */); + assertEquals(1, connection.openStreamCount()); + connection.ping().roundTripTime(); // Prevent the peer from exiting prematurely. + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + } + + @Test public void headers() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.acceptFrame(); // PING + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + peer.sendFrame().headers(0, 1, Arrays.asList("c", "c3po")); + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + connection.ping().roundTripTime(); // Ensure that the HEADERS has been received. + assertEquals(Arrays.asList("a", "android", "c", "c3po"), stream.getResponseHeaders()); + + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + } + + @Test public void headersBeforeReply() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.acceptFrame(); // PING + peer.sendFrame().headers(0, 1, Arrays.asList("c", "c3po")); + peer.acceptFrame(); // RST_STREAM + peer.sendFrame().ping(0, 1); + peer.play(); + + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + connection.ping().roundTripTime(); // Ensure that the HEADERS has been received. + try { + stream.getResponseHeaders(); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage()); } - @Test public void serverCreatesStreamAndClientReplies() throws Exception { - // write the mocking script - peer.sendFrame().synStream(0, 2, 0, 5, 129, Arrays.asList("a", "android")); - peer.acceptFrame(); // SYN_REPLY - peer.play(); + // verify the peer received what was expected + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame ping = peer.takeFrame(); + assertEquals(TYPE_PING, ping.type); + MockSpdyPeer.InFrame rstStream = peer.takeFrame(); + assertEquals(TYPE_RST_STREAM, rstStream.type); + assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode); + } - // play it back - final AtomicInteger receiveCount = new AtomicInteger(); - IncomingStreamHandler handler = new IncomingStreamHandler() { - @Override public void receive(SpdyStream stream) throws IOException { - 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); + @Test public void readSendsWindowUpdate() throws Exception { + // Write the mocking script. + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + for (int i = 0; i < 3; i++) { + peer.sendFrame().data(0, 1, new byte[WINDOW_UPDATE_THRESHOLD]); + peer.acceptFrame(); // WINDOW UPDATE + } + peer.sendFrame().data(FLAG_FIN, 1, new byte[0]); + peer.play(); - } - }; - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(handler) - .build(); + // Play it back. + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); + InputStream in = stream.getInputStream(); + int total = 0; + byte[] buffer = new byte[1024]; + int count; + while ((count = in.read(buffer)) != -1) { + total += count; + if (total == 3 * WINDOW_UPDATE_THRESHOLD) break; + } + assertEquals(-1, in.read()); - // verify the peer received what was expected - MockSpdyPeer.InFrame reply = peer.takeFrame(); - assertEquals(TYPE_SYN_REPLY, reply.type); - assertEquals(0, reply.flags); - assertEquals(2, reply.streamId); - assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock); - assertEquals(1, receiveCount.get()); + // Verify the peer received what was expected. + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + for (int i = 0; i < 3; i++) { + MockSpdyPeer.InFrame windowUpdate = peer.takeFrame(); + assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type); + assertEquals(1, windowUpdate.streamId); + assertEquals(WINDOW_UPDATE_THRESHOLD, windowUpdate.deltaWindowSize); + } + } + + @Test public void writeAwaitsWindowUpdate() throws Exception { + // Write the mocking script. This accepts more data frames than necessary! + peer.acceptFrame(); // SYN_STREAM + for (int i = 0; i < Settings.DEFAULT_INITIAL_WINDOW_SIZE / 1024; i++) { + peer.acceptFrame(); // DATA + } + peer.play(); + + // Play it back. + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + OutputStream out = stream.getOutputStream(); + out.write(new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE]); + interruptAfterDelay(500); + try { + out.write('a'); + out.flush(); + fail(); + } catch (InterruptedIOException expected) { } - @Test public void replyWithNoData() throws Exception { - // write the mocking script - peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android")); - peer.acceptFrame(); // SYN_REPLY - peer.play(); + // Verify the peer received what was expected. + MockSpdyPeer.InFrame synStream = peer.takeFrame(); + assertEquals(TYPE_SYN_STREAM, synStream.type); + MockSpdyPeer.InFrame data = peer.takeFrame(); + assertEquals(TYPE_DATA, data.type); + } - // play it back - final AtomicInteger receiveCount = new AtomicInteger(); - IncomingStreamHandler handler = new IncomingStreamHandler() { - @Override public void receive(SpdyStream stream) throws IOException { - stream.reply(Arrays.asList("b", "banana"), false); - receiveCount.incrementAndGet(); - } - }; - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(handler) - .build(); + @Test public void testTruncatedDataFrame() throws Exception { + // write the mocking script + peer.acceptFrame(); // SYN_STREAM + peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); + peer.sendTruncatedFrame(8 + 100).data(0, 1, new byte[1024]); + peer.play(); - // verify the peer received what was expected - MockSpdyPeer.InFrame reply = peer.takeFrame(); - assertEquals(TYPE_SYN_REPLY, reply.type); - assertEquals(FLAG_FIN, reply.flags); - assertEquals(Arrays.asList("b", "banana"), reply.nameValueBlock); - assertEquals(1, receiveCount.get()); + // play it back + SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); + SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); + assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); + InputStream in = stream.getInputStream(); + try { + Util.readFully(in, new byte[101]); + fail(); + } catch (IOException expected) { + assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage()); } + } - @Test public void noop() throws Exception { - // write the mocking script - peer.acceptFrame(); // NOOP - peer.play(); + private void writeAndClose(SpdyStream stream, String data) throws IOException { + OutputStream out = stream.getOutputStream(); + out.write(data.getBytes("UTF-8")); + out.close(); + } - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - connection.noop(); - - // verify the peer received what was expected - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_NOOP, ping.type); - assertEquals(0, ping.flags); + private void assertStreamData(String expected, InputStream inputStream) throws IOException { + ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + for (int count; (count = inputStream.read(buffer)) != -1; ) { + bytesOut.write(buffer, 0, count); } + String actual = bytesOut.toString("UTF-8"); + assertEquals(expected, actual); + } - @Test public void serverPingsClient() throws Exception { - // write the mocking script - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); // PING - peer.play(); - - // play it back - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - - // verify the peer received what was expected - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - assertEquals(0, ping.flags); - assertEquals(2, ping.streamId); - } - - @Test public void clientPingsServer() throws Exception { - // write the mocking script - peer.acceptFrame(); // PING - peer.sendFrame().ping(0, 1); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - Ping ping = connection.ping(); - assertTrue(ping.roundTripTime() > 0); - assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1)); - - // verify the peer received what was expected - MockSpdyPeer.InFrame pingFrame = peer.takeFrame(); - assertEquals(TYPE_PING, pingFrame.type); - assertEquals(0, pingFrame.flags); - assertEquals(1, pingFrame.streamId); - } - - @Test public void unexpectedPingIsNotReturned() throws Exception { - // write the mocking script - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); // PING - peer.sendFrame().ping(0, 3); // This ping will not be returned. - peer.sendFrame().ping(0, 4); - peer.acceptFrame(); // PING - peer.play(); - - // play it back - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - - // verify the peer received what was expected - MockSpdyPeer.InFrame ping2 = peer.takeFrame(); - assertEquals(2, ping2.streamId); - MockSpdyPeer.InFrame ping4 = peer.takeFrame(); - assertEquals(4, ping4.streamId); - } - - @Test public void serverSendsSettingsToClient() throws Exception { - // write the mocking script - Settings settings = new Settings(); - settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10); - peer.sendFrame().settings(Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS, settings); - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); // PING - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - - peer.takeFrame(); // Guarantees that the Settings frame has been processed. - synchronized (connection) { - assertEquals(10, connection.settings.getMaxConcurrentStreams(-1)); - } - } - - @Test public void multipleSettingsFramesAreMerged() throws Exception { - // write the mocking script - Settings settings1 = new Settings(); - settings1.set(Settings.UPLOAD_BANDWIDTH, PERSIST_VALUE, 100); - settings1.set(Settings.DOWNLOAD_BANDWIDTH, PERSIST_VALUE, 200); - settings1.set(Settings.DOWNLOAD_RETRANS_RATE, 0, 300); - peer.sendFrame().settings(0, settings1); - Settings settings2 = new Settings(); - settings2.set(Settings.DOWNLOAD_BANDWIDTH, 0, 400); - settings2.set(Settings.DOWNLOAD_RETRANS_RATE, PERSIST_VALUE, 500); - settings2.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 600); - peer.sendFrame().settings(0, settings2); - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - - peer.takeFrame(); // Guarantees that the Settings frame has been processed. - synchronized (connection) { - assertEquals(100, connection.settings.getUploadBandwidth(-1)); - assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.UPLOAD_BANDWIDTH)); - assertEquals(400, connection.settings.getDownloadBandwidth(-1)); - assertEquals(0, connection.settings.flags(Settings.DOWNLOAD_BANDWIDTH)); - assertEquals(500, connection.settings.getDownloadRetransRate(-1)); - assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.DOWNLOAD_RETRANS_RATE)); - assertEquals(600, connection.settings.getMaxConcurrentStreams(-1)); - assertEquals(PERSIST_VALUE, connection.settings.flags(Settings.MAX_CONCURRENT_STREAMS)); - } - } - - @Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception { - // write the mocking script - peer.sendFrame().data(SpdyConnection.FLAG_FIN, 42, "bogus".getBytes("UTF-8")); - peer.acceptFrame(); // RST_STREAM - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); // PING - peer.play(); - - // play it back - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - - // verify the peer received what was expected - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(0, rstStream.flags); - assertEquals(42, rstStream.streamId); - assertEquals(RST_INVALID_STREAM, rstStream.statusCode); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(2, ping.streamId); - } - - @Test public void bogusReplyFrameDoesNotDisruptConnection() throws Exception { - // write the mocking script - peer.sendFrame().synReply(0, 42, Arrays.asList("a", "android")); - peer.acceptFrame(); // RST_STREAM - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); // PING - peer.play(); - - // play it back - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - - // verify the peer received what was expected - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(0, rstStream.flags); - assertEquals(42, rstStream.streamId); - assertEquals(RST_INVALID_STREAM, rstStream.statusCode); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(2, ping.streamId); - } - - @Test public void clientClosesClientOutputStream() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); - peer.acceptFrame(); // TYPE_DATA - peer.acceptFrame(); // TYPE_DATA with FLAG_FIN - peer.acceptFrame(); // PING - peer.sendFrame().ping(0, 1); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, false); - OutputStream out = stream.getOutputStream(); - out.write("square".getBytes(UTF_8)); - out.flush(); - assertEquals(1, connection.openStreamCount()); - out.close(); + /** Interrupts the current thread after {@code delayMillis}. */ + private void interruptAfterDelay(final long delayMillis) { + final Thread toInterrupt = Thread.currentThread(); + new Thread("interrupting cow") { + @Override public void run() { try { - out.write("round".getBytes(UTF_8)); - fail(); - } catch (Exception expected) { - assertEquals("stream closed", expected.getMessage()); + Thread.sleep(delayMillis); + toInterrupt.interrupt(); + } catch (InterruptedException e) { + throw new AssertionError(); } - connection.ping().roundTripTime(); // Ensure that the SYN_REPLY has been received. - assertEquals(0, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - assertEquals(FLAG_UNIDIRECTIONAL, synStream.flags); - MockSpdyPeer.InFrame data = peer.takeFrame(); - assertEquals(TYPE_DATA, data.type); - assertEquals(0, data.flags); - assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data)); - MockSpdyPeer.InFrame fin = peer.takeFrame(); - assertEquals(TYPE_DATA, fin.type); - assertEquals(FLAG_FIN, fin.flags); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - assertEquals(1, ping.streamId); - } - - @Test public void serverClosesClientOutputStream() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().rstStream(1, SpdyStream.RST_CANCEL); - peer.acceptFrame(); // PING - peer.sendFrame().ping(0, 1); - peer.acceptFrame(); // DATA - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); - OutputStream out = stream.getOutputStream(); - connection.ping().roundTripTime(); // Ensure that the RST_CANCEL has been received. - try { - out.write("square".getBytes(UTF_8)); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: CANCEL", expected.getMessage()); - } - out.close(); - assertEquals(0, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - assertEquals(0, synStream.flags); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - assertEquals(1, ping.streamId); - MockSpdyPeer.InFrame data = peer.takeFrame(); - assertEquals(TYPE_DATA, data.type); - assertEquals(1, data.streamId); - assertEquals(FLAG_FIN, data.flags); - } - - /** - * Test that the client sends a RST_STREAM if doing so won't disrupt the - * output stream. - */ - @Test public void clientClosesClientInputStream() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.acceptFrame(); // RST_STREAM - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, true); - InputStream in = stream.getInputStream(); - OutputStream out = stream.getOutputStream(); - in.close(); - try { - in.read(); - fail(); - } catch (IOException expected) { - assertEquals("stream closed", expected.getMessage()); - } - try { - out.write('a'); - fail(); - } catch (IOException expected) { - assertEquals("stream finished", expected.getMessage()); - } - assertEquals(0, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - assertEquals(SpdyConnection.FLAG_FIN, synStream.flags); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode); - } - - /** - * Test that the client doesn't send a RST_STREAM if doing so will disrupt - * the output stream. - */ - @Test public void clientClosesClientInputStreamIfOutputStreamIsClosed() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.acceptFrame(); // DATA - peer.acceptFrame(); // DATA with FLAG_FIN - peer.acceptFrame(); // RST_STREAM - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); - InputStream in = stream.getInputStream(); - OutputStream out = stream.getOutputStream(); - in.close(); - try { - in.read(); - fail(); - } catch (IOException expected) { - assertEquals("stream closed", expected.getMessage()); - } - out.write("square".getBytes(UTF_8)); - out.flush(); - out.close(); - assertEquals(0, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - assertEquals(0, synStream.flags); - MockSpdyPeer.InFrame data = peer.takeFrame(); - assertEquals(TYPE_DATA, data.type); - assertTrue(Arrays.equals("square".getBytes("UTF-8"), data.data)); - MockSpdyPeer.InFrame fin = peer.takeFrame(); - assertEquals(TYPE_DATA, fin.type); - assertEquals(FLAG_FIN, fin.flags); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(SpdyStream.RST_CANCEL, rstStream.statusCode); - } - - @Test public void serverClosesClientInputStream() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); - peer.sendFrame().data(FLAG_FIN, 1, "square".getBytes(UTF_8)); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()) - .handler(REJECT_INCOMING_STREAMS) - .build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), false, true); - InputStream in = stream.getInputStream(); - assertStreamData("square", in); - assertEquals(0, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - assertEquals(SpdyConnection.FLAG_FIN, synStream.flags); - } - - @Test public void remoteDoubleSynReply() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - peer.acceptFrame(); // PING - peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); - peer.sendFrame().ping(0, 1); - peer.acceptFrame(); // RST_STREAM - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("c", "cola"), true, true); - assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); - connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received. - try { - stream.getInputStream().read(); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: STREAM_IN_USE", expected.getMessage()); - } - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(1, rstStream.streamId); - assertEquals(0, rstStream.flags); - assertEquals(RST_STREAM_IN_USE, rstStream.statusCode); - } - - @Test public void remoteDoubleSynStream() throws Exception { - // write the mocking script - peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("a", "android")); - peer.acceptFrame(); // SYN_REPLY - peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "banana")); - peer.acceptFrame(); // RST_STREAM - peer.play(); - - // play it back - final AtomicInteger receiveCount = new AtomicInteger(); - IncomingStreamHandler handler = new IncomingStreamHandler() { - @Override public void receive(SpdyStream stream) throws IOException { - receiveCount.incrementAndGet(); - assertEquals(Arrays.asList("a", "android"), stream.getRequestHeaders()); - assertEquals(-1, stream.getRstStatusCode()); - stream.reply(Arrays.asList("c", "cola"), true); - } - }; - new SpdyConnection.Builder(true, peer.openSocket()) - .handler(handler) - .build(); - - // verify the peer received what was expected - MockSpdyPeer.InFrame reply = peer.takeFrame(); - assertEquals(TYPE_SYN_REPLY, reply.type); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(2, rstStream.streamId); - assertEquals(0, rstStream.flags); - assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode); - assertEquals(1, receiveCount.intValue()); - } - - @Test public void remoteSendsDataAfterInFinished() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8")); - peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "c3po".getBytes("UTF-8")); // Ignored. - peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded. - peer.acceptFrame(); // PING - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); - assertStreamData("robot", stream.getInputStream()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - assertEquals(2, ping.streamId); - assertEquals(0, ping.flags); - } - - @Test public void remoteSendsTooMuchData() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana")); - peer.sendFrame().data(0, 1, new byte[64 * 1024 + 1]); - peer.acceptFrame(); // RST_STREAM - peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded. - peer.acceptFrame(); // PING - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); - assertEquals(Arrays.asList("b", "banana"), stream.getResponseHeaders()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(1, rstStream.streamId); - assertEquals(0, rstStream.flags); - assertEquals(RST_FLOW_CONTROL_ERROR, rstStream.statusCode); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - assertEquals(2, ping.streamId); - } - - @Test public void remoteSendsRefusedStreamBeforeReplyHeaders() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().rstStream(1, RST_REFUSED_STREAM); - peer.sendFrame().ping(0, 2); - peer.acceptFrame(); // PING - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); - try { - stream.getResponseHeaders(); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage()); - } - assertEquals(0, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - assertEquals(2, ping.streamId); - assertEquals(0, ping.flags); - } - - @Test public void receiveGoAway() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM 1 - peer.acceptFrame(); // SYN_STREAM 3 - peer.sendFrame().goAway(0, 1, GOAWAY_PROTOCOL_ERROR); - peer.acceptFrame(); // PING - peer.sendFrame().ping(0, 1); - peer.acceptFrame(); // DATA STREAM 1 - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream1 = connection.newStream(Arrays.asList("a", "android"), true, true); - SpdyStream stream2 = connection.newStream(Arrays.asList("b", "banana"), true, true); - connection.ping().roundTripTime(); // Ensure that the GO_AWAY has been received. - stream1.getOutputStream().write("abc".getBytes(UTF_8)); - try { - stream2.getOutputStream().write("abc".getBytes(UTF_8)); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage()); - } - stream1.getOutputStream().write("def".getBytes(UTF_8)); - stream1.getOutputStream().close(); - try { - connection.newStream(Arrays.asList("c", "cola"), true, true); - fail(); - } catch (IOException expected) { - assertEquals("shutdown", expected.getMessage()); - } - assertEquals(1, connection.openStreamCount()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream1 = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream1.type); - MockSpdyPeer.InFrame synStream2 = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream2.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - MockSpdyPeer.InFrame data1 = peer.takeFrame(); - assertEquals(TYPE_DATA, data1.type); - assertEquals(1, data1.streamId); - assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data)); - } - - @Test public void sendGoAway() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM 1 - peer.acceptFrame(); // GOAWAY - peer.acceptFrame(); // PING - peer.sendFrame().synStream(0, 2, 0, 0, 0, Arrays.asList("b", "b")); // Should be ignored! - peer.sendFrame().ping(0, 1); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - connection.newStream(Arrays.asList("a", "android"), true, true); - Ping ping = connection.ping(); - connection.shutdown(GOAWAY_PROTOCOL_ERROR); - assertEquals(1, connection.openStreamCount()); - ping.roundTripTime(); // Prevent the peer from exiting prematurely. - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream1 = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream1.type); - MockSpdyPeer.InFrame pingFrame = peer.takeFrame(); - assertEquals(TYPE_PING, pingFrame.type); - 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 { - // write the mocking script - peer.acceptFrame(); // GOAWAY - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - connection.shutdown(GOAWAY_INTERNAL_ERROR); - try { - connection.ping(); - fail(); - } catch (IOException expected) { - assertEquals("shutdown", expected.getMessage()); - } - - // 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 { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.acceptFrame(); // GOAWAY - peer.acceptFrame(); // RST_STREAM - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("a", "android"), true, true); - assertEquals(1, connection.openStreamCount()); - connection.close(); - assertEquals(0, connection.openStreamCount()); - try { - connection.newStream(Arrays.asList("b", "banana"), true, true); - fail(); - } catch (IOException expected) { - assertEquals("shutdown", expected.getMessage()); - } - try { - stream.getOutputStream().write(0); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: CANCEL", expected.getMessage()); - } - try { - stream.getInputStream().read(); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: CANCEL", expected.getMessage()); - } - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame goaway = peer.takeFrame(); - assertEquals(TYPE_GOAWAY, goaway.type); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(1, rstStream.streamId); - } - - @Test public void closeCancelsPings() throws Exception { - // write the mocking script - peer.acceptFrame(); // PING - peer.acceptFrame(); // GOAWAY - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - Ping ping = connection.ping(); - connection.close(); - assertEquals(-1, ping.roundTripTime()); - } - - @Test public void readTimeoutExpires() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - peer.acceptFrame(); // PING - peer.sendFrame().ping(0, 1); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - stream.setReadTimeout(1000); - InputStream in = stream.getInputStream(); - long startNanos = System.nanoTime(); - try { - in.read(); - fail(); - } catch (IOException expected) { - } - long elapsedNanos = System.nanoTime() - startNanos; - assertEquals(1000d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */); - assertEquals(1, connection.openStreamCount()); - connection.ping().roundTripTime(); // Prevent the peer from exiting prematurely. - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - } - - @Test public void headers() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.acceptFrame(); // PING - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - peer.sendFrame().headers(0, 1, Arrays.asList("c", "c3po")); - peer.sendFrame().ping(0, 1); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - connection.ping().roundTripTime(); // Ensure that the HEADERS has been received. - assertEquals(Arrays.asList("a", "android", "c", "c3po"), stream.getResponseHeaders()); - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - } - - @Test public void headersBeforeReply() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.acceptFrame(); // PING - peer.sendFrame().headers(0, 1, Arrays.asList("c", "c3po")); - peer.acceptFrame(); // RST_STREAM - peer.sendFrame().ping(0, 1); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - connection.ping().roundTripTime(); // Ensure that the HEADERS has been received. - try { - stream.getResponseHeaders(); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage()); - } - - // verify the peer received what was expected - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame ping = peer.takeFrame(); - assertEquals(TYPE_PING, ping.type); - MockSpdyPeer.InFrame rstStream = peer.takeFrame(); - assertEquals(TYPE_RST_STREAM, rstStream.type); - assertEquals(RST_PROTOCOL_ERROR, rstStream.statusCode); - } - - @Test public void readSendsWindowUpdate() throws Exception { - // Write the mocking script. - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - for (int i = 0; i < 3; i++) { - peer.sendFrame().data(0, 1, new byte[WINDOW_UPDATE_THRESHOLD]); - peer.acceptFrame(); // WINDOW UPDATE - } - peer.sendFrame().data(FLAG_FIN, 1, new byte[0]); - peer.play(); - - // Play it back. - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); - InputStream in = stream.getInputStream(); - int total = 0; - byte[] buffer = new byte[1024]; - int count; - while ((count = in.read(buffer)) != -1) { - total += count; - if (total == 3 * WINDOW_UPDATE_THRESHOLD) break; - } - assertEquals(-1, in.read()); - - // Verify the peer received what was expected. - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - for (int i = 0; i < 3; i++) { - MockSpdyPeer.InFrame windowUpdate = peer.takeFrame(); - assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type); - assertEquals(1, windowUpdate.streamId); - assertEquals(WINDOW_UPDATE_THRESHOLD, windowUpdate.deltaWindowSize); - } - } - - @Test public void writeAwaitsWindowUpdate() throws Exception { - // Write the mocking script. This accepts more data frames than necessary! - peer.acceptFrame(); // SYN_STREAM - for (int i = 0; i < Settings.DEFAULT_INITIAL_WINDOW_SIZE / 1024; i++) { - peer.acceptFrame(); // DATA - } - peer.play(); - - // Play it back. - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - OutputStream out = stream.getOutputStream(); - out.write(new byte[Settings.DEFAULT_INITIAL_WINDOW_SIZE]); - interruptAfterDelay(500); - try { - out.write('a'); - out.flush(); - fail(); - } catch (InterruptedIOException expected) { - } - - // Verify the peer received what was expected. - MockSpdyPeer.InFrame synStream = peer.takeFrame(); - assertEquals(TYPE_SYN_STREAM, synStream.type); - MockSpdyPeer.InFrame data = peer.takeFrame(); - assertEquals(TYPE_DATA, data.type); - } - - @Test public void testTruncatedDataFrame() throws Exception { - // write the mocking script - peer.acceptFrame(); // SYN_STREAM - peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android")); - peer.sendTruncatedFrame(8 + 100).data(0, 1, new byte[1024]); - peer.play(); - - // play it back - SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build(); - SpdyStream stream = connection.newStream(Arrays.asList("b", "banana"), true, true); - assertEquals(Arrays.asList("a", "android"), stream.getResponseHeaders()); - InputStream in = stream.getInputStream(); - try { - Util.readFully(in, new byte[101]); - fail(); - } catch (IOException expected) { - assertEquals("stream was reset: PROTOCOL_ERROR", expected.getMessage()); - } - } - - private void writeAndClose(SpdyStream stream, String data) throws IOException { - OutputStream out = stream.getOutputStream(); - out.write(data.getBytes("UTF-8")); - out.close(); - } - - private void assertStreamData(String expected, InputStream inputStream) throws IOException { - ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - for (int count; (count = inputStream.read(buffer)) != -1; ) { - bytesOut.write(buffer, 0, count); - } - String actual = bytesOut.toString("UTF-8"); - assertEquals(expected, actual); - } - - /** - * Interrupts the current thread after {@code delayMillis}. - */ - private void interruptAfterDelay(final long delayMillis) { - final Thread toInterrupt = Thread.currentThread(); - new Thread("interrupting cow") { - @Override public void run() { - try { - Thread.sleep(delayMillis); - toInterrupt.interrupt(); - } catch (InterruptedException e) { - throw new AssertionError(); - } - } - }.start(); - } + } + }.start(); + } } 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 f81c05841..7371f2e3d 100644 --- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java +++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyServer.java @@ -34,137 +34,128 @@ import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import org.eclipse.jetty.npn.NextProtoNego; -/** - * A basic SPDY server that serves the contents of a local directory. - */ +/** A basic SPDY server that serves the contents of a local directory. */ public final class SpdyServer implements IncomingStreamHandler { - private final File baseDirectory; - private SSLSocketFactory sslSocketFactory; + private final File baseDirectory; + private SSLSocketFactory sslSocketFactory; - public SpdyServer(File baseDirectory) { - this.baseDirectory = baseDirectory; + public SpdyServer(File baseDirectory) { + this.baseDirectory = baseDirectory; + } + + public void useHttps(SSLSocketFactory sslSocketFactory) { + this.sslSocketFactory = sslSocketFactory; + } + + private void run() throws Exception { + ServerSocket serverSocket = new ServerSocket(8888); + serverSocket.setReuseAddress(true); + + while (true) { + Socket socket = serverSocket.accept(); + if (sslSocketFactory != null) { + socket = doSsl(socket); + } + new SpdyConnection.Builder(false, socket).handler(this).build(); + } + } + + private Socket doSsl(Socket socket) throws IOException { + SSLSocket sslSocket = + (SSLSocket) sslSocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(), + socket.getPort(), true); + sslSocket.setUseClientMode(false); + NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() { + @Override public void unsupported() { + System.out.println("UNSUPPORTED"); + } + @Override public List protocols() { + return Arrays.asList("spdy/3"); + } + @Override public void protocolSelected(String protocol) { + System.out.println("PROTOCOL SELECTED: " + protocol); + } + }); + return sslSocket; + } + + @Override public void receive(final SpdyStream stream) throws IOException { + List requestHeaders = stream.getRequestHeaders(); + String path = null; + for (int i = 0; i < requestHeaders.size(); i += 2) { + String s = requestHeaders.get(i); + if (":path".equals(s)) { + path = requestHeaders.get(i + 1); + break; + } } - public void useHttps(SSLSocketFactory sslSocketFactory) { - this.sslSocketFactory = sslSocketFactory; + if (path == null) { + // TODO: send bad request error + throw new AssertionError(); } - private void run() throws Exception { - ServerSocket serverSocket = new ServerSocket(8888); - serverSocket.setReuseAddress(true); + File file = new File(baseDirectory + path); - while (true) { - Socket socket = serverSocket.accept(); - if (sslSocketFactory != null) { - socket = doSsl(socket); - } - new SpdyConnection.Builder(false, socket).handler(this).build(); - } + if (file.isDirectory()) { + serveDirectory(stream, file.list()); + } else if (file.exists()) { + serveFile(stream, file); + } else { + send404(stream, path); + } + } + + private void send404(SpdyStream stream, String path) throws IOException { + List responseHeaders = + Arrays.asList(":status", "404", ":version", "HTTP/1.1", "content-type", "text/plain"); + stream.reply(responseHeaders, true); + OutputStream out = stream.getOutputStream(); + String text = "Not found: " + path; + out.write(text.getBytes("UTF-8")); + out.close(); + } + + private void serveDirectory(SpdyStream stream, String[] files) throws IOException { + List responseHeaders = + Arrays.asList(":status", "200", ":version", "HTTP/1.1", "content-type", + "text/html; charset=UTF-8"); + stream.reply(responseHeaders, true); + OutputStream out = stream.getOutputStream(); + Writer writer = new OutputStreamWriter(out, "UTF-8"); + for (String file : files) { + writer.write("" + file + "
"); + } + writer.close(); + } + + private void serveFile(SpdyStream stream, File file) throws IOException { + InputStream in = new FileInputStream(file); + byte[] buffer = new byte[8192]; + stream.reply( + Arrays.asList(":status", "200", ":version", "HTTP/1.1", "content-type", contentType(file)), + true); + OutputStream out = stream.getOutputStream(); + int count; + while ((count = in.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + out.close(); + } + + private String contentType(File file) { + return file.getName().endsWith(".html") ? "text/html" : "text/plain"; + } + + public static void main(String... args) throws Exception { + if (args.length != 1 || args[0].startsWith("-")) { + System.out.println("Usage: SpdyServer "); + return; } - private Socket doSsl(Socket socket) throws IOException { - SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket, - socket.getInetAddress().getHostAddress(), socket.getPort(), true); - sslSocket.setUseClientMode(false); - NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() { - @Override public void unsupported() { - System.out.println("UNSUPPORTED"); - } - @Override public List protocols() { - return Arrays.asList("spdy/3"); - } - @Override public void protocolSelected(String protocol) { - System.out.println("PROTOCOL SELECTED: " + protocol); - } - }); - return sslSocket; - } - - @Override public void receive(final SpdyStream stream) throws IOException { - List requestHeaders = stream.getRequestHeaders(); - String path = null; - for (int i = 0; i < requestHeaders.size(); i += 2) { - String s = requestHeaders.get(i); - if (":path".equals(s)) { - path = requestHeaders.get(i + 1); - break; - } - } - - if (path == null) { - // TODO: send bad request error - throw new AssertionError(); - } - - File file = new File(baseDirectory + path); - - if (file.isDirectory()) { - serveDirectory(stream, file.list()); - } else if (file.exists()) { - serveFile(stream, file); - } else { - send404(stream, path); - } - } - - private void send404(SpdyStream stream, String path) throws IOException { - List responseHeaders = Arrays.asList( - ":status", "404", - ":version", "HTTP/1.1", - "content-type", "text/plain" - ); - stream.reply(responseHeaders, true); - OutputStream out = stream.getOutputStream(); - String text = "Not found: " + path; - out.write(text.getBytes("UTF-8")); - out.close(); - } - - private void serveDirectory(SpdyStream stream, String[] files) throws IOException { - List responseHeaders = Arrays.asList( - ":status", "200", - ":version", "HTTP/1.1", - "content-type", "text/html; charset=UTF-8" - ); - stream.reply(responseHeaders, true); - OutputStream out = stream.getOutputStream(); - Writer writer = new OutputStreamWriter(out, "UTF-8"); - for (String file : files) { - writer.write("" + file + "
"); - } - writer.close(); - } - - private void serveFile(SpdyStream stream, File file) throws IOException { - InputStream in = new FileInputStream(file); - byte[] buffer = new byte[8192]; - stream.reply(Arrays.asList( - ":status", "200", - ":version", "HTTP/1.1", - "content-type", contentType(file) - ), true); - OutputStream out = stream.getOutputStream(); - int count; - while ((count = in.read(buffer)) != -1) { - out.write(buffer, 0, count); - } - out.close(); - } - - private String contentType(File file) { - return file.getName().endsWith(".html") ? "text/html" : "text/plain"; - } - - public static void main(String... args) throws Exception { - if (args.length != 1 || args[0].startsWith("-")) { - System.out.println("Usage: SpdyServer "); - return; - } - - SpdyServer server = new SpdyServer(new File(args[0])); - SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()) - .build(); - server.useHttps(sslContext.getSocketFactory()); - server.run(); - } + SpdyServer server = new SpdyServer(new File(args[0])); + SSLContext sslContext = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build(); + server.useHttps(sslContext.getSocketFactory()); + server.run(); + } }