Upgrade to the latest OkHttp, with changes through 2013-Feb-01.
This is OkHttp commit 2137b7a72518cfac088757ef6d0fb3d085bf6e3d
Most notable changes are improvements to ConnectionPool (fixing
infinite loops!) and upgrading to SPDY/3.
Change-Id: Icd9e5e3857a45f9b68a164b920d33f48cdddb780
diff --git a/README.md b/README.md
index 9793fcf..c3a44ee 100644
--- a/README.md
+++ b/README.md
@@ -23,16 +23,33 @@
Known Issues
------------
-The SPDY implementation is incomplete:
-
-* Settings frames are not honored. Flow control is not implemented.
-* It assumes a well-behaved peer. If the peer sends an invalid frame, OkHttp's SPDY client will not respond with the required `RST` frame.
-
OkHttp uses the platform's [ProxySelector][2]. Prior to Android 4.0, `ProxySelector` didn't honor the `proxyHost` and `proxyPort` system properties for HTTPS connections. Work around this by specifying the `https.proxyHost` and `https.proxyPort` system properties when using a proxy with HTTPS.
OkHttp's test suite creates an in-process HTTPS server. Prior to Android 2.3, SSL server sockets were broken, and so HTTPS tests will time out when run on such devices.
+Building
+--------
+
+### On the Desktop
+Run OkHttp tests on the desktop with Maven. Running SPDY tests on the desktop uses [Jetty-NPN](http://wiki.eclipse.org/Jetty/Feature/NPN) which requires OpenJDK 7+.
+```
+mvn clean test
+```
+
+### On a Device
+Test on a USB-attached Android using [Vogar](https://code.google.com/p/vogar/). Unfortunately `dx` requires that you build with Java 6, otherwise the test class will be silently omitted from the `.dex` file.
+```
+mvn clean
+mvn package -DskipTests
+vogar \
+ --classpath ~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.47/bcprov-jdk15on-1.47.jar \
+ --classpath ~/.m2/repository/com/google/mockwebserver/mockwebserver/20130122/mockwebserver-20130122.jar \
+ --classpath target/okhttp-0.9-SNAPSHOT.jar \
+ ./src/test/java
+```
+
+
License
-------
diff --git a/checkstyle.xml b/checkstyle.xml
index b54793c..2079edc 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -1,120 +1,132 @@
-<?xml version="1.0"?>
-<!DOCTYPE module PUBLIC
- "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
- "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
-
-<module name="Checker">
- <module name="NewlineAtEndOfFile"/>
- <module name="FileLength"/>
- <module name="FileTabCharacter"/>
-
- <!-- Trailing spaces -->
- <module name="RegexpSingleline">
- <property name="format" value="\s+$"/>
- <property name="message" value="Line has trailing spaces."/>
- </module>
-
- <module name="TreeWalker">
- <property name="cacheFile" value="${checkstyle.cache.file}"/>
-
- <!-- Checks for Javadoc comments. -->
- <!-- See http://checkstyle.sf.net/config_javadoc.html -->
- <!--module name="JavadocMethod"/-->
- <!--module name="JavadocType"/-->
- <!--module name="JavadocVariable"/-->
- <module name="JavadocStyle"/>
-
-
- <!-- Checks for Naming Conventions. -->
- <!-- See http://checkstyle.sf.net/config_naming.html -->
- <module name="ConstantName"/>
- <module name="LocalFinalVariableName"/>
- <module name="LocalVariableName"/>
- <module name="MemberName"/>
- <module name="MethodName"/>
- <module name="PackageName"/>
- <module name="ParameterName"/>
- <module name="StaticVariableName"/>
- <module name="TypeName"/>
-
-
- <!-- Checks for imports -->
- <!-- See http://checkstyle.sf.net/config_import.html -->
- <module name="AvoidStarImport"/>
- <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
- <module name="RedundantImport"/>
- <module name="UnusedImports"/>
-
-
- <!-- Checks for Size Violations. -->
- <!-- See http://checkstyle.sf.net/config_sizes.html -->
- <module name="LineLength">
- <property name="max" value="120"/>
- </module>
- <module name="MethodLength"/>
- <module name="ParameterNumber"/>
-
-
- <!-- Checks for whitespace -->
- <!-- See http://checkstyle.sf.net/config_whitespace.html -->
- <module name="GenericWhitespace"/>
- <module name="EmptyForIteratorPad"/>
- <module name="MethodParamPad"/>
- <module name="NoWhitespaceAfter"/>
- <module name="NoWhitespaceBefore"/>
- <module name="OperatorWrap"/>
- <module name="ParenPad"/>
- <module name="TypecastParenPad"/>
- <module name="WhitespaceAfter"/>
- <module name="WhitespaceAround"/>
-
-
- <!-- Modifier Checks -->
- <!-- See http://checkstyle.sf.net/config_modifiers.html -->
- <!--module name="ModifierOrder"/-->
- <module name="RedundantModifier"/>
-
-
- <!-- Checks for blocks. You know, those {}'s -->
- <!-- See http://checkstyle.sf.net/config_blocks.html -->
- <module name="AvoidNestedBlocks"/>
- <!--module name="EmptyBlock"/-->
- <module name="LeftCurly"/>
- <!--<module name="NeedBraces"/>-->
- <module name="RightCurly"/>
-
-
- <!-- Checks for common coding problems -->
- <!-- See http://checkstyle.sf.net/config_coding.html -->
- <!--module name="AvoidInlineConditionals"/-->
- <module name="CovariantEquals"/>
- <module name="DoubleCheckedLocking"/>
- <module name="EmptyStatement"/>
- <!--<module name="EqualsAvoidNull"/>-->
- <module name="EqualsHashCode"/>
- <!--module name="HiddenField"/-->
- <module name="IllegalInstantiation"/>
- <!--module name="InnerAssignment"/-->
- <!--module name="MagicNumber"/-->
- <!--module name="MissingSwitchDefault"/-->
- <module name="RedundantThrows"/>
- <module name="SimplifyBooleanExpression"/>
- <module name="SimplifyBooleanReturn"/>
-
- <!-- Checks for class design -->
- <!-- See http://checkstyle.sf.net/config_design.html -->
- <!--module name="DesignForExtension"/-->
- <module name="FinalClass"/>
- <module name="HideUtilityClassConstructor"/>
- <module name="InterfaceIsType"/>
- <!--s/module name="VisibilityModifier"/-->
-
-
- <!-- Miscellaneous other checks. -->
- <!-- See http://checkstyle.sf.net/config_misc.html -->
- <module name="ArrayTypeStyle"/>
- <!--module name="FinalParameters"/-->
- <!--module name="TodoComment"/-->
- <module name="UpperEll"/>
- </module>
-</module>
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<module name="Checker">
+ <module name="NewlineAtEndOfFile"/>
+ <module name="FileLength"/>
+ <module name="FileTabCharacter"/>
+
+ <!-- Trailing spaces -->
+ <module name="RegexpSingleline">
+ <property name="format" value="\s+$"/>
+ <property name="message" value="Line has trailing spaces."/>
+ </module>
+
+ <!-- Space after 'for' and 'if' -->
+ <module name="RegexpSingleline">
+ <property name="format" value="^\s*(for|if)\b[^ ]"/>
+ <property name="message" value="Space needed before opening parenthesis."/>
+ </module>
+
+ <!-- For each spacing -->
+ <module name="RegexpSingleline">
+ <property name="format" value="^\s*for \(.*?([^ ]:|:[^ ])"/>
+ <property name="message" value="Space needed around ':' character."/>
+ </module>
+
+ <module name="TreeWalker">
+ <property name="cacheFile" value="${checkstyle.cache.file}"/>
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <!--module name="JavadocMethod"/-->
+ <!--module name="JavadocType"/-->
+ <!--module name="JavadocVariable"/-->
+ <module name="JavadocStyle"/>
+
+
+ <!-- Checks for Naming Conventions. -->
+ <!-- See http://checkstyle.sf.net/config_naming.html -->
+ <!--<module name="ConstantName"/>-->
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+
+ <!-- Checks for imports -->
+ <!-- See http://checkstyle.sf.net/config_import.html -->
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/> <!-- defaults to sun.* packages -->
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <module name="LineLength">
+ <property name="max" value="100"/>
+ </module>
+ <module name="MethodLength"/>
+ <module name="ParameterNumber"/>
+
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="GenericWhitespace"/>
+ <!--<module name="EmptyForIteratorPad"/>-->
+ <module name="MethodParamPad"/>
+ <!--<module name="NoWhitespaceAfter"/>-->
+ <!--<module name="NoWhitespaceBefore"/>-->
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="TypecastParenPad"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround"/>
+
+
+ <!-- Modifier Checks -->
+ <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+ <module name="ModifierOrder"/>
+ <module name="RedundantModifier"/>
+
+
+ <!-- Checks for blocks. You know, those {}'s -->
+ <!-- See http://checkstyle.sf.net/config_blocks.html -->
+ <module name="AvoidNestedBlocks"/>
+ <!--module name="EmptyBlock"/-->
+ <module name="LeftCurly"/>
+ <!--<module name="NeedBraces"/>-->
+ <module name="RightCurly"/>
+
+
+ <!-- Checks for common coding problems -->
+ <!-- See http://checkstyle.sf.net/config_coding.html -->
+ <!--module name="AvoidInlineConditionals"/-->
+ <module name="CovariantEquals"/>
+ <module name="DoubleCheckedLocking"/>
+ <module name="EmptyStatement"/>
+ <!--<module name="EqualsAvoidNull"/>-->
+ <module name="EqualsHashCode"/>
+ <!--module name="HiddenField"/-->
+ <module name="IllegalInstantiation"/>
+ <!--module name="InnerAssignment"/-->
+ <!--module name="MagicNumber"/-->
+ <!--module name="MissingSwitchDefault"/-->
+ <module name="RedundantThrows"/>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+
+ <!-- Checks for class design -->
+ <!-- See http://checkstyle.sf.net/config_design.html -->
+ <!--module name="DesignForExtension"/-->
+ <!--<module name="FinalClass"/>-->
+ <module name="HideUtilityClassConstructor"/>
+ <module name="InterfaceIsType"/>
+ <!--module name="VisibilityModifier"/-->
+
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="ArrayTypeStyle"/>
+ <!--module name="FinalParameters"/-->
+ <!--module name="TodoComment"/-->
+ <module name="UpperEll"/>
+ </module>
+</module>
diff --git a/pom.xml b/pom.xml
index b95912d..0afcaa6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,7 @@
</parent>
<groupId>com.squareup</groupId>
<artifactId>okhttp</artifactId>
- <version>0.8-SNAPSHOT</version>
+ <version>0.9-SNAPSHOT</version>
<packaging>jar</packaging>
<name>okhttp</name>
@@ -38,11 +38,11 @@
<!-- Compilation -->
<java.version>1.6</java.version>
<npn.version>8.1.2.v20120308</npn.version>
- <mockwebserver.version>20120905</mockwebserver.version>
+ <mockwebserver.version>20130122</mockwebserver.version>
<bouncycastle.version>1.47</bouncycastle.version>
<!-- Test Dependencies -->
- <junit.version>3.8.2</junit.version>
+ <junit.version>4.10</junit.version>
</properties>
<scm>
@@ -68,6 +68,7 @@
<groupId>org.mortbay.jetty.npn</groupId>
<artifactId>npn-boot</artifactId>
<version>${npn.version}</version>
+ <optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.mockwebserver</groupId>
@@ -101,70 +102,6 @@
</configuration>
</plugin>
<plugin>
- <groupId>org.sonatype.plugins</groupId>
- <artifactId>jarjar-maven-plugin</artifactId>
- <version>1.5</version>
- <executions>
- <execution>
- <phase>package</phase>
- <goals>
- <goal>jarjar</goal>
- </goals>
- <configuration>
- <includes>
- <include>asm:asm</include>
- <include>org.sonatype.sisu.inject:cglib</include>
- </includes>
- <rules>
- <rule>
- <pattern>libcore.**</pattern>
- <result>com.squareup.okhttp.libcore.@1</result>
- </rule>
- </rules>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-surefire-plugin</artifactId>
- <version>2.9</version>
- <configuration>
- <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
- </configuration>
- </plugin>
- <plugin>
- <!--
- OkHttp requires with javac >= 1.7 for syncFlush on DeflaterOutputStream.
- Its language version must be <= 1.6 for dx.
-
- Running this code on Java 6 or earlier will fail at runtime due to the missing
- syncFlush API.
-
- Dalvik's core library includes syncFlush, but with an @hide tag so that it doesn't
- show up in the documentation or the android.jar stubs. This code works fine on
- Dalvik.
- -->
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-enforcer-plugin</artifactId>
- <version>1.1</version>
- <executions>
- <execution>
- <id>enforce-java</id>
- <goals>
- <goal>enforce</goal>
- </goals>
- <configuration>
- <rules>
- <requireJavaVersion>
- <version>[1.7.0,)</version>
- </requireJavaVersion>
- </rules>
- </configuration>
- </execution>
- </executions>
- </plugin>
- <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>2.9.1</version>
@@ -183,6 +120,14 @@
</execution>
</executions>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <version>2.9</version>
+ <configuration>
+ <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
+ </configuration>
+ </plugin>
</plugins>
</build>
</project>
diff --git a/src/main/java/com/squareup/okhttp/Address.java b/src/main/java/com/squareup/okhttp/Address.java
index 430eff5..cd41ac9 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 @@
* {@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 61f74d1..1679cd5 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.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.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 @@
* There are tradeoffs when selecting which options to include when negotiating
* a secure connection to a remote host. Newer TLS options are quite useful:
* <ul>
- * <li>Server Name Indication (SNI) enables one IP address to negotiate secure
- * connections for multiple domain names.
- * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
- * for both HTTP and SPDY transports.
+ * <li>Server Name Indication (SNI) enables one IP address to negotiate secure
+ * connections for multiple domain names.
+ * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
+ * for both HTTP and SPDY transports.
* </ul>
* Unfortunately, older HTTPS servers refuse to connect when such options are
* presented. Rather than avoiding these options entirely, this class allows a
@@ -64,241 +65,257 @@
* should the attempt fail.
*/
public final class Connection implements Closeable {
- private static final byte[] NPN_PROTOCOLS = new byte[] {
- 6, 's', 'p', 'd', 'y', '/', '2',
- 8, 'h', 't', 't', 'p', '/', '1', '.', '1',
- };
- private static final byte[] SPDY2 = new byte[] {
- 's', 'p', 'd', 'y', '/', '2',
- };
- 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 recycled = false;
- private SpdyConnection spdyConnection;
- private int httpMinorVersion = 1; // Assume HTTP/1.1
+ 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 {
- socket = (proxy.type() != Proxy.Type.HTTP)
- ? new Socket(proxy)
- : new Socket();
- socket.connect(inetSocketAddress, connectTimeout);
- socket.setSoTimeout(readTimeout);
- in = socket.getInputStream();
- out = socket.getOutputStream();
+ // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
+ if (!isSpdy()) {
+ int bufferSize = 128;
+ in = new BufferedInputStream(in, bufferSize);
+ }
+ }
- if (address.sslSocketFactory != null) {
- upgradeToTls(tunnelRequest);
- }
+ /**
+ * Create an {@code SSLSocket} and perform the TLS handshake and certificate
+ * validation.
+ */
+ private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
+ Platform platform = Platform.get();
- // Buffer the socket stream to permit efficient parsing of HTTP headers and chunk sizes.
- if (!isSpdy()) {
- int bufferSize = 128;
- in = new BufferedInputStream(in, bufferSize);
- }
+ // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+ if (requiresTunnel()) {
+ makeTunnel(tunnelRequest);
}
- /**
- * 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, SPDY2)) {
- 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"));
- }
- }
+ // 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);
}
- @Override public void close() throws IOException {
- socket.close();
+ if (modernTls) {
+ platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS);
}
- /**
- * Returns the proxy that this connection is using.
- *
- * <strong>Warning:</strong> 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;
+ // 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");
}
- public Address getAddress() {
- return address;
- }
+ out = sslSocket.getOutputStream();
+ in = sslSocket.getInputStream();
- public InetSocketAddress getSocketAddress() {
- return inetSocketAddress;
+ 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"));
+ }
}
+ }
- public boolean isModernTls() {
- return modernTls;
+ /** 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.
+ *
+ * <strong>Warning:</strong> 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 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 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());
+ }
}
-
- /**
- * Returns true if this connection has been used to satisfy an earlier
- * HTTP request/response pair.
- *
- * <p>The HTTP client treats recycled and non-recycled connections
- * differently. I/O failures on recycled connections are often temporary:
- * the remote peer may have closed the socket because it was idle for an
- * extended period of time. When fresh connections suffer similar failures
- * the problem is fatal and the request is not retried.
- */
- public boolean isRecycled() {
- return recycled;
- }
-
- public void setRecycled() {
- this.recycled = true;
- }
-
- /**
- * Returns true if this connection is eligible to be reused for another
- * request/response pair.
- */
- protected boolean isEligibleForRecycling() {
- return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
- }
-
- /**
- * 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;
- }
-
- /**
- * 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 afb0e58..be4eb1d 100644
--- a/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -1,41 +1,34 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import java.net.SocketException;
import java.util.ArrayList;
-import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+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 Address} may share a {@link Connection}.
- * This class implements the policy of which connections to keep open for future
- * use.
+ * 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.
*
* <p>The {@link #getDefault() system-wide default} uses system properties for
* tuning parameters:
* <ul>
- * <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
- * pooled at all. Default is true.
- * <li>{@code http.maxConnections} maximum number of connections to each
- * address. Default is 5.
+ * <li>{@code http.keepAlive} true if HTTP and SPDY connections should be
+ * pooled at all. Default is true.
+ * <li>{@code http.maxConnections} maximum number of idle connections to
+ * each to keep in the pool. Default is 5.
+ * <li>{@code http.keepAliveDuration} Time in milliseconds to keep the
+ * connection alive in the pool before closing it. Default is 5 minutes.
+ * This property isn't used by {@code HttpURLConnection}.
* </ul>
*
* <p>The default instance <i>doesn't</i> adjust its configuration as system
@@ -43,127 +36,207 @@
* parameters do so before making HTTP connections, and that this class is
* initialized lazily.
*/
-public final class ConnectionPool {
- private static final ConnectionPool systemDefault;
- static {
- String keepAlive = System.getProperty("http.keepAlive");
- String maxConnections = System.getProperty("http.maxConnections");
- if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
- systemDefault = new ConnectionPool(0);
- } else if (maxConnections != null) {
- systemDefault = new ConnectionPool(Integer.parseInt(maxConnections));
- } else {
- systemDefault = new ConnectionPool(5);
- }
+public class ConnectionPool {
+ private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
+ private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
+
+ private static final ConnectionPool systemDefault;
+
+ static {
+ String keepAlive = System.getProperty("http.keepAlive");
+ String keepAliveDuration = System.getProperty("http.keepAliveDuration");
+ String maxIdleConnections = System.getProperty("http.maxConnections");
+ long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
+ : DEFAULT_KEEP_ALIVE_DURATION_MS;
+ if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
+ systemDefault = new ConnectionPool(0, keepAliveDurationMs);
+ } else if (maxIdleConnections != null) {
+ systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs);
+ } else {
+ systemDefault = new ConnectionPool(5, keepAliveDurationMs);
}
+ }
- /** The maximum number of idle connections for each address. */
- private final int maxConnections;
- private final HashMap<Address, List<Connection>> connectionPool
- = new HashMap<Address, List<Connection>>();
+ /** The maximum number of idle connections for each address. */
+ private final int maxIdleConnections;
+ private final long keepAliveDurationNs;
- public ConnectionPool(int maxConnections) {
- this.maxConnections = maxConnections;
- }
+ private final LinkedList<Connection> connections = new LinkedList<Connection>();
- public static ConnectionPool getDefault() {
- return systemDefault;
- }
-
- /**
- * Returns a recycled connection to {@code address}, or null if no such
- * connection exists.
- */
- public Connection get(Address address) {
- // First try to reuse an existing HTTP connection.
- synchronized (connectionPool) {
- List<Connection> connections = connectionPool.get(address);
- while (connections != null) {
- Connection connection = connections.get(connections.size() - 1);
- if (!connection.isSpdy()) {
- connections.remove(connections.size() - 1);
- }
- if (connections.isEmpty()) {
- connectionPool.remove(address);
- connections = null;
- }
- if (!connection.isEligibleForRecycling()) {
- Util.closeQuietly(connection);
- continue;
- }
- try {
- Platform.get().tagSocket(connection.getSocket());
- } catch (SocketException e) {
- // When unable to tag, skip recycling and close
- Platform.get().logW("Unable to tagSocket(): " + e);
- Util.closeQuietly(connection);
- continue;
- }
- return connection;
- }
- }
- return null;
- }
-
- /**
- * Gives {@code connection} to the pool. The pool may store the connection,
- * or close it, as its policy describes.
- *
- * <p>It is an error to use {@code connection} after calling this method.
- */
- public void recycle(Connection connection) {
- if (connection.isSpdy()) {
- return;
+ /** We use a single background thread to cleanup expired connections. */
+ private final ExecutorService executorService =
+ new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
+ private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() {
+ @Override public Void call() throws Exception {
+ List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
+ int idleConnectionCount = 0;
+ synchronized (ConnectionPool.this) {
+ for (Iterator<Connection> i = connections.descendingIterator(); i.hasNext(); ) {
+ Connection connection = i.next();
+ if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
+ i.remove();
+ expiredConnections.add(connection);
+ if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
+ } else if (connection.isIdle()) {
+ idleConnectionCount++;
+ }
}
+ for (Iterator<Connection> i = connections.descendingIterator();
+ i.hasNext() && idleConnectionCount > maxIdleConnections; ) {
+ Connection connection = i.next();
+ if (connection.isIdle()) {
+ expiredConnections.add(connection);
+ i.remove();
+ --idleConnectionCount;
+ }
+ }
+ }
+ for (Connection expiredConnection : expiredConnections) {
+ Util.closeQuietly(expiredConnection);
+ }
+ return null;
+ }
+ };
+
+ public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
+ this.maxIdleConnections = maxIdleConnections;
+ this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
+ }
+
+ /**
+ * Returns a snapshot of the connections in this pool, ordered from newest to
+ * oldest. Waits for the cleanup callable to run if it is currently scheduled.
+ */
+ List<Connection> getConnections() {
+ waitForCleanupCallableToRun();
+ synchronized (this) {
+ return new ArrayList<Connection>(connections);
+ }
+ }
+
+ /**
+ * Blocks until the executor service has processed all currently enqueued
+ * jobs.
+ */
+ private void waitForCleanupCallableToRun() {
+ try {
+ executorService.submit(new Runnable() {
+ @Override public void run() {
+ }
+ }).get();
+ } catch (Exception e) {
+ throw new AssertionError();
+ }
+ }
+
+ public static ConnectionPool getDefault() {
+ return systemDefault;
+ }
+
+ /** Returns total number of connections in the pool. */
+ public synchronized int getConnectionCount() {
+ return connections.size();
+ }
+
+ /** Returns total number of spdy connections in the pool. */
+ public synchronized int getSpdyConnectionCount() {
+ int total = 0;
+ for (Connection connection : connections) {
+ if (connection.isSpdy()) total++;
+ }
+ return total;
+ }
+
+ /** Returns total number of http connections in the pool. */
+ public synchronized int getHttpConnectionCount() {
+ int total = 0;
+ for (Connection connection : connections) {
+ if (!connection.isSpdy()) total++;
+ }
+ return total;
+ }
+
+ /** Returns a recycled connection to {@code address}, or null if no such connection exists. */
+ public synchronized Connection get(Address address) {
+ Connection foundConnection = null;
+ for (Iterator<Connection> i = connections.descendingIterator(); i.hasNext(); ) {
+ Connection connection = i.next();
+ if (!connection.getAddress().equals(address)
+ || !connection.isAlive()
+ || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
+ continue;
+ }
+ i.remove();
+ if (!connection.isSpdy()) {
try {
- Platform.get().untagSocket(connection.getSocket());
+ Platform.get().tagSocket(connection.getSocket());
} catch (SocketException e) {
- // When unable to remove tagging, skip recycling and close
- Platform.get().logW("Unable to untagSocket(): " + e);
- Util.closeQuietly(connection);
- return;
+ Util.closeQuietly(connection);
+ // When unable to tag, skip recycling and close
+ Platform.get().logW("Unable to tagSocket(): " + e);
+ continue;
}
-
- if (maxConnections > 0 && connection.isEligibleForRecycling()) {
- Address address = connection.getAddress();
- synchronized (connectionPool) {
- List<Connection> connections = connectionPool.get(address);
- if (connections == null) {
- connections = new ArrayList<Connection>();
- connectionPool.put(address, connections);
- }
- if (connections.size() < maxConnections) {
- connection.setRecycled();
- connections.add(connection);
- return; // keep the connection open
- }
- }
- }
-
- // don't close streams while holding a lock!
- Util.closeQuietly(connection);
+ }
+ foundConnection = connection;
+ break;
}
- /**
- * Shares the SPDY connection with the pool. Callers to this method may
- * continue to use {@code connection}.
- */
- public void share(Connection connection) {
- if (!connection.isSpdy()) {
- throw new IllegalArgumentException();
- }
- if (maxConnections <= 0 || !connection.isEligibleForRecycling()) {
- return;
- }
- Address address = connection.getAddress();
- synchronized (connectionPool) {
- List<Connection> connections = connectionPool.get(address);
- if (connections == null) {
- connections = new ArrayList<Connection>(1);
- connections.add(connection);
- connectionPool.put(address, connections);
- }
- }
+ if (foundConnection != null && foundConnection.isSpdy()) {
+ connections.addFirst(foundConnection); // Add it back after iteration.
}
+
+ executorService.submit(connectionsCleanupCallable);
+ return foundConnection;
+ }
+
+ /**
+ * Gives {@code connection} to the pool. The pool may store the connection,
+ * or close it, as its policy describes.
+ *
+ * <p>It is an error to use {@code connection} after calling this method.
+ */
+ public void recycle(Connection connection) {
+ executorService.submit(connectionsCleanupCallable);
+
+ if (connection.isSpdy()) {
+ return;
+ }
+
+ if (!connection.isAlive()) {
+ Util.closeQuietly(connection);
+ return;
+ }
+
+ try {
+ Platform.get().untagSocket(connection.getSocket());
+ } catch (SocketException e) {
+ // When unable to remove tagging, skip recycling and close.
+ Platform.get().logW("Unable to untagSocket(): " + e);
+ Util.closeQuietly(connection);
+ return;
+ }
+
+ synchronized (this) {
+ connections.addFirst(connection);
+ connection.resetIdleStartTime();
+ }
+ }
+
+ /**
+ * Shares the SPDY connection with the pool. Callers to this method may
+ * continue to use {@code connection}.
+ */
+ public void maybeShare(Connection connection) {
+ executorService.submit(connectionsCleanupCallable);
+ if (!connection.isSpdy()) {
+ // Only SPDY connections are sharable.
+ return;
+ }
+ if (connection.isAlive()) {
+ synchronized (this) {
+ connections.addFirst(connection);
+ }
+ }
+ }
}
diff --git a/src/main/java/com/squareup/okhttp/OkHttpClient.java b/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 2205487..4cc5ec6 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.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 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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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.
+ *
+ * <p>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 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.
- *
- * <p>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.
- *
- * <p>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.
- *
- * <p>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.
- *
- * <p>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.
- *
- * <p>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.
- *
- * <p>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 c0f7c56..b7e3801 100644
--- a/src/main/java/com/squareup/okhttp/OkResponseCache.java
+++ b/src/main/java/com/squareup/okhttp/OkResponseCache.java
@@ -26,19 +26,13 @@
*/
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 83388d6..4eca172 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 39a820c..5260b87 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 @@
* 5.2</a>.
*/
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 458e536..79cd020 100644
--- a/src/main/java/com/squareup/okhttp/internal/Base64.java
+++ b/src/main/java/com/squareup/okhttp/internal/Base64.java
@@ -16,150 +16,149 @@
*/
/**
-* @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;
+
/**
* <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> 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 96f6d96..00fe2f1 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.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 @@
* entry may have only one editor at one time; if a value is not available to be
* edited then {@link #edit} will return null.
* <ul>
- * <li>When an entry is being <strong>created</strong> it is necessary to
- * supply a full set of values; the empty value should be used as a
- * placeholder if necessary.
- * <li>When an entry is being <strong>edited</strong>, it is not necessary
- * to supply data for every value; values default to their previous
- * value.
+ * <li>When an entry is being <strong>created</strong> it is necessary to
+ * supply a full set of values; the empty value should be used as a
+ * placeholder if necessary.
+ * <li>When an entry is being <strong>edited</strong>, it is not necessary
+ * to supply data for every value; values default to their previous
+ * value.
* </ul>
* 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,759 +88,739 @@
* 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
- * looks like this:
- * libcore.io.DiskLruCache
- * 1
- * 100
- * 2
- *
- * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
- * DIRTY 335c4c6028171cfddfbaae1a9c313c52
- * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
- * REMOVE 335c4c6028171cfddfbaae1a9c313c52
- * DIRTY 1ab96a171faeeee38496d8b330771a7a
- * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
- * READ 335c4c6028171cfddfbaae1a9c313c52
- * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
- *
- * The first five lines of the journal form its header. They are the
- * constant string "libcore.io.DiskLruCache", the disk cache's version,
- * the application's version, the value count, and a blank line.
- *
- * Each of the subsequent lines in the file is a record of the state of a
- * cache entry. Each line contains space-separated values: a state, a key,
- * and optional state-specific values.
- * o DIRTY lines track that an entry is actively being created or updated.
- * Every successful DIRTY action should be followed by a CLEAN or REMOVE
- * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
- * temporary files may need to be deleted.
- * o CLEAN lines track a cache entry that has been successfully published
- * and may be read. A publish line is followed by the lengths of each of
- * its values.
- * o READ lines track accesses for LRU.
- * o REMOVE lines track entries that have been deleted.
- *
- * The journal file is appended to as cache operations occur. The journal may
- * occasionally be compacted by dropping redundant lines. A temporary file named
- * "journal.tmp" will be used during compaction; that file should be deleted if
- * it exists when the cache is opened.
- */
+ // This cache uses a journal file named "journal". A typical journal file
+ // looks like this:
+ // libcore.io.DiskLruCache
+ // 1
+ // 100
+ // 2
+ //
+ // CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
+ // DIRTY 335c4c6028171cfddfbaae1a9c313c52
+ // CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
+ // REMOVE 335c4c6028171cfddfbaae1a9c313c52
+ // DIRTY 1ab96a171faeeee38496d8b330771a7a
+ // CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
+ // READ 335c4c6028171cfddfbaae1a9c313c52
+ // READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
+ //
+ // The first five lines of the journal form its header. They are the
+ // constant string "libcore.io.DiskLruCache", the disk cache's version,
+ // the application's version, the value count, and a blank line.
+ //
+ // Each of the subsequent lines in the file is a record of the state of a
+ // cache entry. Each line contains space-separated values: a state, a key,
+ // and optional state-specific values.
+ // o DIRTY lines track that an entry is actively being created or updated.
+ // Every successful DIRTY action should be followed by a CLEAN or REMOVE
+ // action. DIRTY lines without a matching CLEAN or REMOVE indicate that
+ // temporary files may need to be deleted.
+ // o CLEAN lines track a cache entry that has been successfully published
+ // and may be read. A publish line is followed by the lengths of each of
+ // its values.
+ // o READ lines track accesses for LRU.
+ // o REMOVE lines track entries that have been deleted.
+ //
+ // The journal file is appended to as cache operations occur. The journal may
+ // occasionally be compacted by dropping redundant lines. A temporary file named
+ // "journal.tmp" will be used during compaction; that file should be deleted if
+ // 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<String, Entry> lruEntries
- = new LinkedHashMap<String, Entry>(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<String, Entry> lruEntries =
+ new LinkedHashMap<String, Entry>(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<Runnable>());
- private final Callable<Void> cleanupCallable = new Callable<Void>() {
- @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<Runnable>());
+ private final Callable<Void> cleanupCallable = new Callable<Void>() {
+ @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<Entry> 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();
- }
-
- 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');
- }
- }
-
- writer.close();
- journalFileTmp.renameTo(journalFile);
- journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+ 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);
}
+ }
- 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;
+ /**
+ * 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<Entry> 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];
}
-
- 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];
- 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++;
+ } else {
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');
+ for (int t = 0; t < valueCount; t++) {
+ deleteIfExists(entry.getCleanFile(t));
+ deleteIfExists(entry.getDirtyFile(t));
}
+ i.remove();
+ }
+ }
+ }
- if (size > maxSize || journalRebuildRequired()) {
- executorService.submit(cleanupCallable);
+ /**
+ * 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();
+ }
+
+ 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');
+ }
+ }
+
+ writer.close();
+ journalFileTmp.renameTo(journalFile);
+ journalWriter = new BufferedWriter(new FileWriter(journalFile, true));
+ }
+
+ 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];
+ 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<Entry>(lruEntries.values())) {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.abort();
+ }
+ }
+ trimToSize();
+ journalWriter.close();
+ journalWriter = null;
+ }
+
+ private void trimToSize() throws IOException {
+ while (size > maxSize) {
+ Map.Entry<String, 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;
}
/**
- * We only rebuild the journal when it will halve the size of the journal
- * and eliminate at least 2000 ops.
+ * 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.
*/
- private boolean journalRebuildRequired() {
- final int redundantOpCompactThreshold = 2000;
- return redundantOpCount >= redundantOpCompactThreshold
- && redundantOpCount >= lruEntries.size();
+ 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];
}
/**
- * 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.
+ * Returns an unbuffered input stream to read the last committed value,
+ * or null if no value has been committed.
*/
- public synchronized boolean remove(String key) throws IOException {
- checkNotClosed();
- validateKey(key);
- Entry entry = lruEntries.get(key);
- if (entry == null || entry.currentEditor != null) {
- return false;
+ public InputStream newInputStream(int index) throws IOException {
+ synchronized (DiskLruCache.this) {
+ if (entry.currentEditor != this) {
+ throw new IllegalStateException();
}
-
- 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;
+ if (!entry.readable) {
+ return null;
}
-
- redundantOpCount++;
- journalWriter.append(REMOVE + ' ' + key + '\n');
- lruEntries.remove(key);
-
- if (journalRebuildRequired()) {
- executorService.submit(cleanupCallable);
- }
-
- return true;
+ return new FileInputStream(entry.getCleanFile(index));
+ }
}
/**
- * Returns true if this cache has been closed.
+ * Returns the last committed value as a string, or null if no value
+ * has been committed.
*/
- public boolean isClosed() {
- return journalWriter == null;
- }
-
- private void checkNotClosed() {
- if (journalWriter == null) {
- throw new IllegalStateException("cache is closed");
- }
+ public String getString(int index) throws IOException {
+ InputStream in = newInputStream(index);
+ return in != null ? inputStreamToString(in) : null;
}
/**
- * Force buffered operations to the filesystem.
+ * 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 synchronized void flush() throws IOException {
- checkNotClosed();
- trimToSize();
- journalWriter.flush();
+ 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);
+ }
}
/**
- * Closes this cache. Stored values will remain on the filesystem.
+ * 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 synchronized void close() throws IOException {
- if (journalWriter == null) {
- return; // already closed
- }
- for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
- if (entry.currentEditor != null) {
- entry.currentEditor.abort();
- }
- }
- trimToSize();
- journalWriter.close();
- journalWriter = null;
- }
-
- private void trimToSize() throws IOException {
- while (size > maxSize) {
- Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
- remove(toEvict.getKey());
- }
+ public void commit() throws IOException {
+ if (hasErrors) {
+ completeEdit(this, false);
+ remove(entry.key); // the previous entry is stale
+ } else {
+ completeEdit(this, true);
+ }
}
/**
- * 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.
+ * Aborts this edit. This releases the edit lock so another edit may be
+ * started on the same key.
*/
- public void delete() throws IOException {
- close();
- Util.deleteContents(directory);
+ public void abort() throws IOException {
+ completeEdit(this, false);
}
- 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 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];
}
- private static String inputStreamToString(InputStream in) throws IOException {
- return Util.readFully(new InputStreamReader(in, UTF_8));
+ public String getLengths() throws IOException {
+ StringBuilder result = new StringBuilder();
+ for (long size : lengths) {
+ result.append(' ').append(size);
+ }
+ return result.toString();
}
- /**
- * 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;
+ /** Set lengths using decimal numbers like "10123". */
+ private void setLengths(String[] strings) throws IOException {
+ if (strings.length != valueCount) {
+ throw invalidLengths(strings);
+ }
- private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
- this.key = key;
- this.sequenceNumber = sequenceNumber;
- this.ins = ins;
+ try {
+ for (int i = 0; i < strings.length; i++) {
+ lengths[i] = Long.parseLong(strings[i]);
}
-
- /**
- * 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);
- }
- }
+ } catch (NumberFormatException e) {
+ throw invalidLengths(strings);
+ }
}
- /**
- * 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 IOException invalidLengths(String[] strings) throws IOException {
+ throw new IOException("unexpected journal line: " + Arrays.toString(strings));
}
- 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 getCleanFile(int i) {
+ return new File(directory, key + "." + i);
}
+
+ 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 37fa609..69b2d37 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 @@
* 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 ab71c62..75cd66f 100644
--- a/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -47,297 +47,297 @@
* 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<DeflaterOutputStream> deflaterConstructor;
+ private Constructor<DeflaterOutputStream> 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<DeflaterOutputStream> 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<DeflaterOutputStream> 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");
- Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
- Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
- return new JdkWithJettyNpnPlatform(putMethod, getMethod, clientProviderClass);
- } 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) {
+ try {
+ List<String> strings = new ArrayList<String>();
+ for (int i = 0; i < npnProtocols.length; ) {
+ int length = npnProtocols[i++];
+ strings.add(new String(npnProtocols, i, length, "US-ASCII"));
+ i += length;
}
-
- @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);
- }
- }
+ 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;
-
- public JdkWithJettyNpnPlatform(
- Method putMethod, Method getMethod, Class<?> clientProviderClass) {
- this.putMethod = putMethod;
- this.getMethod = getMethod;
- this.clientProviderClass = clientProviderClass;
+ @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<String> strings = new ArrayList<String>();
- 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}, 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<String> 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<String> 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<String> clientProtocols;
- private boolean unsupported;
- private String selected;
-
- public JettyNpnProvider(List<String> clientProtocols) {
- this.clientProtocols = clientProtocols;
- }
-
- @Override public Object invoke(Object proxy, Method method, Object[] args)
- throws Throwable {
- String methodName = method.getName();
- Class<?> returnType = method.getReturnType();
- 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("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];
- System.out.println("CLIENT PROTOCOLS: " + clientProtocols + " SERVER PROTOCOLS: " + serverProtocols);
- this.selected = clientProtocols.get(0);
- return selected;
- } 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 9011b2c..93f1754 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.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,186 @@
* 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();
+ /**
+ * 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;
}
- 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");
- }
+ }
- 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();
}
-
- /**
- * 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;
- }
+ 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 f47362c..994a646 100644
--- a/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -24,293 +24,313 @@
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringWriter;
+import java.net.Socket;
import java.net.URI;
import java.net.URL;
import java.nio.ByteOrder;
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 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<byte[]> skipBuffer = new AtomicReference<byte[]>();
+ /** A cheap and type-safe constant for the UTF-8 Charset. */
+ public static final Charset UTF_8 = Charset.forName("UTF-8");
+ private static AtomicReference<byte[]> skipBuffer = new AtomicReference<byte[]>();
- 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.
+ *
+ * <p>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 'closeable', ignoring any checked exceptions. Does nothing if '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 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.
- *
- * <p>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;
+ }
+ };
+ }
}
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 d915552..187f3b6 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 @@
* 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 reuseSocket) throws IOException {
- if (cacheRequest != null) {
- cacheBody.close();
- }
- httpEngine.release(reuseSocket);
+ 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.
- *
- * <p>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(false);
+ /**
+ * 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.
+ *
+ * <p>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 5d835cc..90675b0 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 @@
* 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 0dd096e..12e6409 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 @@
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 70104a5..4ccd12a 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 @@
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,158 +27,149 @@
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<Challenge> 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<Challenge> 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<Challenge> parseChallenges(
- RawHeaders responseHeaders, String challengeHeader) {
- /*
- * auth-scheme = token
- * auth-param = token "=" ( token | quoted-string )
- * challenge = auth-scheme 1*SP 1#auth-param
- * realm = "realm" "=" realm-value
- * realm-value = quoted-string
- */
- List<Challenge> result = new ArrayList<Challenge>();
- 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, " ");
+ return null;
+ }
- String scheme = value.substring(tokenStart, pos).trim();
- pos = HeaderParser.skipWhitespace(value, pos);
+ 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());
+ }
- // 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
+ /**
+ * Parse RFC 2617 challenges. This API is only interested in the scheme
+ * name and realm.
+ */
+ private static List<Challenge> parseChallenges(RawHeaders responseHeaders,
+ String challengeHeader) {
+ // auth-scheme = token
+ // auth-param = token "=" ( token | quoted-string )
+ // challenge = auth-scheme 1*SP 1#auth-param
+ // realm = "realm" "=" realm-value
+ // realm-value = quoted-string
+ List<Challenge> result = new ArrayList<Challenge>();
+ 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, " ");
- if (!value.regionMatches(pos, "realm=\"", 0, "realm=\"".length())) {
- break; // Unexpected challenge parameter; give up!
- }
+ String scheme = value.substring(tokenStart, pos).trim();
+ pos = HeaderParser.skipWhitespace(value, pos);
- 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));
- }
+ // 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!
}
- 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 41f03fa..acb5fda 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 @@
*/
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<DateFormat> STANDARD_DATE_FORMAT
- = new ThreadLocal<DateFormat>() {
+ /**
+ * 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<DateFormat> STANDARD_DATE_FORMAT =
+ new ThreadLocal<DateFormat>() {
@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 22483ac..8d40705 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -22,12 +22,9 @@
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.ResponseSource;
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 com.squareup.okhttp.internal.Dns;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -50,19 +47,23 @@
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:
* <ol>
- * <li>It is created.
- * <li>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.
- * <li>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.
+ * <li>It is created.
+ * <li>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.
+ * <li>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.
* </ol>
*
* <p>The request and response may be served by the HTTP response cache, by the
@@ -74,602 +75,606 @@
* required, use {@link #automaticallyReleaseConnectionToPool()}.
*/
public class HttpEngine {
- private static final CacheResponse GATEWAY_TIMEOUT_RESPONSE = new CacheResponse() {
- @Override public Map<String, List<String>> getHeaders() throws IOException {
- Map<String, List<String>> result = new HashMap<String, List<String>>();
- 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<String, List<String>> getHeaders() throws IOException {
+ Map<String, List<String>> result = new HashMap<String, List<String>>();
+ 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();
+ // The raw response source may require the network, but the request
+ // headers may forbid network use. In that case, dispose of the network
+ // 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;
+ }
+ }
+
+ /**
+ * 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;
+ }
+
+ CacheResponse candidate =
+ policy.responseCache.get(uri, method, requestHeaders.getHeaders().toMultimap(false));
+ if (candidate == null) {
+ return;
+ }
+
+ Map<String, List<String>> 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();
+ }
+ }
+
+ 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();
+ }
+ }
+
+ /** 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;
+ }
+
+ // 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()) {
+ // If the response was transparently gzipped, remove the gzip header field
+ // so clients don't double decompress. http://b/3009828
+ //
+ // Also remove the Content-Length in this case because it contains the
+ // length 528 of the gzipped response. This isn't terribly useful and is
+ // dangerous because 529 clients can query the content length, but not
+ // the content encoding.
+ responseHeaders.stripContentEncoding();
+ responseHeaders.stripContentLength();
+ 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;
+ }
+
+ 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;
+ }
+
+ /**
+ * Populates requestHeaders with defaults and cookies.
+ *
+ * <p>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.getUserAgent() == null) {
+ requestHeaders.setUserAgent(getDefaultUserAgent());
+ }
+
+ 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)));
+ }
+ }
+
+ /**
+ * 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").
+ *
+ * <p>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;
+ }
+
+ 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) policy.responseCache).trackResponse(responseSource);
+ OkResponseCache httpResponseCache = (OkResponseCache) policy.responseCache;
+ httpResponseCache.trackConditionalCacheHit();
+ httpResponseCache.update(cacheResponse, getHttpConnectionToCache());
}
-
- /*
- * The raw response source may require the network, but the request
- * headers may forbid network use. In that case, dispose of the network
- * 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;
- }
+ return;
+ } else {
+ Util.closeQuietly(cachedResponseBody);
+ }
}
- /**
- * 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;
- }
-
- CacheResponse candidate = policy.responseCache.get(uri, method,
- requestHeaders.getHeaders().toMultimap(false));
- if (candidate == null) {
- return;
- }
-
- Map<String, List<String>> 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();
- }
+ if (hasResponseBody()) {
+ maybeCache(); // reentrant. this calls into user code which may call back into this!
}
- private void sendSocketRequest() throws IOException {
- if (connection == null) {
- connect();
- }
+ initContentStream(transport.getTransferStream(cacheRequest));
+ }
- if (transport != null) {
- throw new IllegalStateException();
- }
+ protected TunnelRequest getTunnelConfig() {
+ return null;
+ }
- 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();
- }
+ public void receiveHeaders(RawHeaders headers) throws IOException {
+ CookieHandler cookieHandler = policy.cookieHandler;
+ if (cookieHandler != null) {
+ cookieHandler.put(uri, headers.toMultimap(true));
}
-
- /**
- * 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.isRecycled()) {
- connection.connect(policy.getConnectTimeout(), policy.getReadTimeout(),
- getTunnelConfig());
- if (connection.isSpdy()) {
- policy.connectionPool.share(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) {
- }
-
- /**
- * @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;
- }
-
- public final boolean hasRecycledConnection() {
- return connection != null && connection.isRecycled();
- }
-
- /**
- * 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 reusable) {
- // If the response body comes from the cache, close it.
- if (responseBodyIn == cachedResponseBody) {
- Util.closeQuietly(responseBodyIn);
- }
-
- if (!connectionReleased && connection != null) {
- connectionReleased = true;
-
- if (!reusable || !transport.makeReusable(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()) {
- /*
- * 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;
- }
- }
-
- /**
- * 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 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;
- }
-
- /**
- * Populates requestHeaders with defaults and cookies.
- *
- * <p>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.getUserAgent() == null) {
- requestHeaders.setUserAgent(getDefaultUserAgent());
- }
-
- 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)));
- }
- }
-
- /**
- * 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").
- *
- * <p>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;
- }
-
- 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(true);
- 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;
- }
+ }
}
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 4bdd8fc..8735166 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 @@
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,555 @@
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<String, List<String>> 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<String, List<String>> 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);
+ 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;
}
- @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")) {
- /*
- * 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;
- }
+ HttpEngine httpEngine = getHttpEngine(httpConnection);
+ if (httpEngine == null) {
+ // Don't cache unless the HTTP implementation is ours.
+ 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);
- }
+ ResponseHeaders response = httpEngine.getResponseHeaders();
+ if (response.hasVaryAll()) {
+ 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) {
- }
+ 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;
}
+ }
- 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;
- }
+ /**
+ * 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 DiskLruCache getCache() {
- return cache;
+ 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 getWriteAbortCount() {
- return writeAbortCount;
+ 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 int getWriteSuccessCount() {
- return writeSuccessCount;
- }
+ public DiskLruCache getCache() {
+ return cache;
+ }
- public synchronized void trackResponse(ResponseSource source) {
- requestCount++;
+ public synchronized int getWriteAbortCount() {
+ return writeAbortCount;
+ }
- switch (source) {
- case CACHE:
- hitCount++;
- break;
- case CONDITIONAL_CACHE:
- case NETWORK:
- networkCount++;
- break;
- }
- }
+ public synchronized int getWriteSuccessCount() {
+ return writeSuccessCount;
+ }
- public synchronized void trackConditionalCacheHit() {
+ 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 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<String, List<String>> requestHeaders) {
- return this.uri.equals(uri.toString())
- && this.requestMethod.equals(requestMethod)
- && new ResponseHeaders(uri, responseHeaders)
- .varyMatches(varyHeaders.toMultimap(false), requestHeaders);
- }
- }
+ 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;
/**
- * 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:
+ * <pre>{@code
+ * 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
+ * }</pre>
+ *
+ * <p>A typical HTTPS file looks like this:
+ * <pre>{@code
+ * 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
+ * }</pre>
+ * 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.
+ *
+ * <p>Next is the response status line, followed by the number of HTTP
+ * response header lines, followed by those lines.
+ *
+ * <p>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);
+ if (isHttps()) {
+ HttpsURLConnection httpsConnection = (HttpsURLConnection) httpConnection;
+ cipherSuite = httpsConnection.getCipherSuite();
+ Certificate[] peerCertificatesNonFinal = null;
+ try {
+ peerCertificatesNonFinal = httpsConnection.getServerCertificates();
+ } catch (SSLPeerUnverifiedException ignored) {
}
-
- @Override public Map<String, List<String>> getHeaders() {
- return entry.responseHeaders.toMultimap(true);
- }
-
- @Override public InputStream getBody() {
- return in;
- }
+ 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<String, List<String>> 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<Certificate> 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<Certificate> 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<String, List<String>> 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<String, List<String>> 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<String, List<String>> getHeaders() {
+ return entry.responseHeaders.toMultimap(true);
+ }
+
+ @Override public InputStream getBody() {
+ return in;
+ }
+
+ @Override public String getCipherSuite() {
+ return entry.cipherSuite;
+ }
+
+ @Override public List<Certificate> 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<Certificate> 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 1c73423..dd7a38d 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -18,542 +18,482 @@
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;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CacheRequest;
-import java.net.CookieHandler;
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 = 100;
- 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.
- *
- * <p>For streaming requests with a body, headers must be prepared
- * <strong>before</strong> the output stream has been written to. Otherwise
- * the body would need to be buffered!
- *
- * <p>For non-streaming requests with a body, headers must be prepared
- * <strong>after</strong> 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 {
- if (httpEngine.sentRequestMillis != -1) {
- throw new IllegalStateException();
- }
- httpEngine.sentRequestMillis = System.currentTimeMillis();
+ // 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();
+ }
- int contentLength = httpEngine.requestHeaders.getContentLength();
- RawHeaders headersToSend = httpEngine.requestHeaders.getHeaders();
- byte[] bytes = headersToSend.toBytes();
+ @Override public void flushRequest() throws IOException {
+ requestOut.flush();
+ requestOut = socketOut;
+ }
- if (contentLength != -1 && bytes.length + contentLength <= MAX_REQUEST_BUFFER_LENGTH) {
- requestOut = new BufferedOutputStream(socketOut, bytes.length + contentLength);
- }
+ @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+ requestBody.writeToSocket(requestOut);
+ }
- requestOut.write(bytes);
+ /**
+ * Prepares the HTTP headers and sends them to the server.
+ *
+ * <p>For streaming requests with a body, headers must be prepared
+ * <strong>before</strong> the output stream has been written to. Otherwise
+ * the body would need to be buffered!
+ *
+ * <p>For non-streaming requests with a body, headers must be prepared
+ * <strong>after</strong> 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());
- 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;
}
- private void receiveHeaders(RawHeaders headers) throws IOException {
- CookieHandler cookieHandler = httpEngine.policy.cookieHandler;
- if (cookieHandler != null) {
- cookieHandler.put(httpEngine.uri, headers.toMultimap(true));
- }
+ // We cannot reuse sockets that have incomplete output.
+ if (requestBodyOut != null && !((AbstractHttpOutputStream) requestBodyOut).closed) {
+ return false;
}
- public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) {
- // 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);
+ }
+
+ if (httpEngine.responseHeaders.isChunked()) {
+ return new ChunkedInputStream(socketIn, cacheRequest, this);
+ }
+
+ 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);
+ }
+
+ /** 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);
}
/**
- * 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.
+ * 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 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;
- }
+ 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 InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
- if (!httpEngine.hasResponseBody()) {
- return new FixedLengthInputStream(socketIn, cacheRequest, httpEngine, 0);
- }
+ @Override public synchronized void write(byte[] buffer, int offset, int count)
+ throws IOException {
+ checkNotClosed();
+ checkOffsetAndCount(buffer.length, offset, count);
- if (httpEngine.responseHeaders.isChunked()) {
- return new ChunkedInputStream(socketIn, cacheRequest, this);
- }
+ while (count > 0) {
+ int numBytesWritten;
- 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);
- }
-
- /**
- * 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);
- }
-
- /**
- * 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(true);
- }
- }
-
- @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(true);
- }
- 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);
- transport.receiveHeaders(rawResponseHeaders);
- endOfInput(true);
- }
- }
-
- @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();
}
- /**
- * An HTTP payload terminated by the end of the socket stream.
- */
- private static final class UnknownLengthHttpInputStream extends AbstractHttpInputStream {
- private boolean inputExhausted;
-
- private 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 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 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 aec7b4e..40cd516 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 @@
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.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,439 +57,427 @@
* is currently connected to a server.
*/
public class HttpURLConnectionImpl extends HttpURLConnection {
- /**
- * HTTP 1.1 doesn't specify how many redirects to follow, but HTTP/1.0
- * recommended 5. http://www.w3.org/Protocols/HTTP/1.0/spec.html#Code3xx
- */
- private static final int MAX_REDIRECTS = 5;
+ /**
+ * 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<String, List<String>> getHeaderFields() {
+ try {
+ return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ @Override public final Map<String, List<String>> 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);
+ HttpEngine response = getResponse();
+
+ // if the requested file does not exist, throw an exception formerly the
+ // Error page from the server was returned if the requested file was
+ // text/html this has changed to return FileNotFoundException for all
+ // file types
+ if (getResponseCode() >= HTTP_BAD_REQUEST) {
+ throw new FileNotFoundException(url.toString());
}
- @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(false);
+ 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;
+ }
+ }
+
+ /**
+ * 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;
}
- /**
- * 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;
- }
- }
+ while (true) {
+ if (!execute(true)) {
+ continue;
+ }
- /**
- * 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<String, List<String>> getHeaderFields() {
- try {
- return getResponse().getResponseHeaders().getHeaders().toMultimap(true);
- } catch (IOException e) {
- return null;
- }
- }
-
- @Override public final Map<String, List<String>> 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();
-
- /*
- * if the requested file does not exist, throw an exception formerly the
- * Error page from the server was returned if the requested file was
- * text/html this has changed to return FileNotFoundException for all
- * file types
- */
- 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;
- }
- }
-
- /**
- * 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;
- }
-
- /*
- * The first request was insufficient. Prepare for another...
- */
- 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;
- }
-
- if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
- throw new HttpRetryException("Cannot retry streamed HTTP body",
- httpEngine.getResponseCode());
- }
-
- if (retry == Retry.DIFFERENT_CONNECTION) {
- httpEngine.automaticallyReleaseConnectionToPool();
- }
-
- httpEngine.release(true);
-
- 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) {
- throw e; // Without a route selector, we can't retry.
- } else if (httpEngine.connection != null) {
- routeSelector.connectFailed(httpEngine.connection, e);
- }
-
- // The connection failure isn't fatal if there's another route to attempt.
- OutputStream requestBody = httpEngine.getRequestBody();
- if (routeSelector.hasNext() && isRecoverable(e)
- && (requestBody == null || requestBody instanceof RetryableOutputStream)) {
- httpEngine.release(false);
- 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() {
+ 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();
+
+ // 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;
+ }
+
+ if (requestBody != null && !(requestBody instanceof RetryableOutputStream)) {
+ throw new HttpRetryException("Cannot retry streamed HTTP body",
+ httpEngine.getResponseCode());
+ }
+
+ if (retry == Retry.DIFFERENT_CONNECTION) {
+ httpEngine.automaticallyReleaseConnectionToPool();
+ }
+
+ httpEngine.release(false);
+
+ httpEngine = newHttpEngine(retryMethod, rawRequestHeaders, httpEngine.getConnection(),
+ (RetryableOutputStream) requestBody);
}
+ }
- enum Retry {
- NONE,
- SAME_CONNECTION,
- DIFFERENT_CONNECTION
+ /**
+ * 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;
}
+ }
- /**
- * 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;
+ 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;
+ }
- 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");
- }
- 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;
- }
+ HttpEngine getHttpEngine() {
+ return httpEngine;
+ }
- default:
- return Retry.NONE;
+ 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;
- 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");
+ case HTTP_MULT_CHOICE:
+ case HTTP_MOVED_PERM:
+ case HTTP_MOVED_TEMP:
+ case HTTP_SEE_OTHER:
+ if (!getInstanceFollowRedirects()) {
+ return Retry.NONE;
}
- if (field == null) {
- throw new NullPointerException("field == null");
+ if (++redirectionCount > MAX_REDIRECTS) {
+ throw new ProtocolException("Too many redirects: " + redirectionCount);
}
- rawRequestHeaders.set(field, newValue);
- }
+ 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;
+ }
- @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 b0fc73b..8212903 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 @@
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.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<Certificate> 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<Certificate> 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<String, List<String>> getHeaderFields() {
+ return delegate.getHeaderFields();
+ }
+
+ @Override
+ public Map<String, List<String>> 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<Certificate> 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<Certificate> 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 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
- 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<String, List<String>> getHeaderFields() {
- return delegate.getHeaderFields();
- }
-
- @Override
- public Map<String, List<String>> 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());
- }
- }
+ }
}
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 edb1436..6799eeb 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/RawHeaders.java
@@ -51,389 +51,372 @@
* leading or trailing whitespace.
*/
public final class RawHeaders {
- private static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
- // @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 static final Comparator<String> FIELD_NAME_COMPARATOR = new Comparator<String>() {
+ // @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<String> namesAndValues = new ArrayList<String>(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);
+ }
+ 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;
+ }
+
+ 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);
+ }
+
+ /**
+ * @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));
+ }
+ }
+
+ /** 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) {
+ // 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;
+ }
+ 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<String> 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<String> 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<String, List<String>> toMultimap(boolean response) {
+ Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
+ for (int i = 0; i < namesAndValues.size(); i += 2) {
+ String fieldName = namesAndValues.get(i);
+ String value = namesAndValues.get(i + 1);
+
+ List<String> allValues = new ArrayList<String>();
+ List<String> 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<String, List<String>> map, boolean response)
+ throws IOException {
+ if (!response) throw new UnsupportedOperationException();
+ RawHeaders result = new RawHeaders();
+ for (Entry<String, List<String>> entry : map.entrySet()) {
+ String fieldName = entry.getKey();
+ List<String> 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<String> toNameValueBlock() {
+ Set<String> names = new HashSet<String>();
+ List<String> result = new ArrayList<String>();
+ 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;
}
- };
-
- private final List<String> namesAndValues = new ArrayList<String>(20);
- private String requestLine;
- private String statusLine;
- private int httpMinorVersion = 1;
- private int responseCode = -1;
- private String responseMessage;
-
- public RawHeaders() {
+ }
}
+ return result;
+ }
- public RawHeaders(RawHeaders copyFrom) {
- namesAndValues.addAll(copyFrom.namesAndValues);
- requestLine = copyFrom.requestLine;
- statusLine = copyFrom.statusLine;
- httpMinorVersion = copyFrom.httpMinorVersion;
- responseCode = copyFrom.responseCode;
- responseMessage = copyFrom.responseMessage;
+ public static RawHeaders fromNameValueBlock(List<String> nameValueBlock) {
+ if (nameValueBlock.size() % 2 != 0) {
+ throw new IllegalArgumentException("Unexpected name value block: " + nameValueBlock);
}
-
- /**
- * 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);
+ 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();
}
- 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;
+ result.namesAndValues.add(name);
+ result.namesAndValues.add(values.substring(start, end));
+ start = end + 1;
+ }
}
-
- 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);
- }
-
- /**
- * @param method like "GET", "POST", "HEAD", etc.
- * @param scheme like "https"
- * @param url like "/foo/bar.html"
- * @param version like "HTTP/1.1"
- */
- public void addSpdyRequestHeaders(String method, String scheme, String url, String version) {
- // TODO: populate the statusLine for the client's benefit?
- add("method", method);
- add("scheme", scheme);
- add("url", url);
- add("version", version);
- }
-
- 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));
- }
- }
-
- /**
- * 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) {
- /*
- * 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;
- }
- 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<String> 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<String> 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<String, List<String>> toMultimap(boolean response) {
- Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
- for (int i = 0; i < namesAndValues.size(); i += 2) {
- String fieldName = namesAndValues.get(i);
- String value = namesAndValues.get(i + 1);
-
- List<String> allValues = new ArrayList<String>();
- List<String> 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<String, List<String>> map, boolean response)
- throws IOException {
- if (!response) throw new UnsupportedOperationException();
- RawHeaders result = new RawHeaders();
- for (Entry<String, List<String>> entry : map.entrySet()) {
- String fieldName = entry.getKey();
- List<String> 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<String> toNameValueBlock() {
- Set<String> names = new HashSet<String>();
- List<String> result = new ArrayList<String>();
- 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 ignored when layering HTTP over SPDY.
- if (name.equals("connection") || name.equals("accept-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<String> 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 2c12b95..2544cee 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.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<String, List<String>> allCookieHeaders) {
- for (Map.Entry<String, List<String>> 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<String, List<String>> allCookieHeaders) {
+ for (Map.Entry<String, List<String>> 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 d333d8b..4e520db 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,462 @@
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<String> varyFields = Collections.emptySet();
+ /** Case-insensitive set of field names. */
+ private Set<String> 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>(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);
- }
-
- public void stripContentEncoding() {
- contentEncoding = null;
- headers.removeAll("Content-Encoding");
- }
-
- 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 Date getServedDate() {
- return servedDate;
- }
-
- public Date getLastModified() {
- return lastModified;
- }
-
- public Date getExpires() {
- return expires;
- }
-
- public boolean isNoCache() {
- return noCache;
- }
-
- public boolean isNoStore() {
- return noStore;
- }
-
- public int getMaxAgeSeconds() {
- return maxAgeSeconds;
- }
-
- public int getSMaxAgeSeconds() {
- return sMaxAgeSeconds;
- }
-
- public boolean isPublic() {
- return isPublic;
- }
-
- public boolean isMustRevalidate() {
- return mustRevalidate;
- }
-
- public String getEtag() {
- return etag;
- }
-
- public Set<String> getVaryFields() {
- return varyFields;
- }
-
- public String getContentEncoding() {
- return contentEncoding;
- }
-
- public int getContentLength() {
- return contentLength;
- }
-
- public String getConnection() {
- return connection;
- }
-
- 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 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;
+ 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;
}
- return 0;
+ } 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>(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 boolean isContentEncodingGzip() {
+ return "gzip".equalsIgnoreCase(contentEncoding);
+ }
+
+ public void stripContentEncoding() {
+ contentEncoding = null;
+ headers.removeAll("Content-Encoding");
+ }
+
+ public void stripContentLength() {
+ contentLength = -1;
+ headers.removeAll("Content-Length");
+ }
+
+ 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 Date getServedDate() {
+ return servedDate;
+ }
+
+ public Date getLastModified() {
+ return lastModified;
+ }
+
+ public Date getExpires() {
+ return expires;
+ }
+
+ public boolean isNoCache() {
+ return noCache;
+ }
+
+ public boolean isNoStore() {
+ return noStore;
+ }
+
+ public int getMaxAgeSeconds() {
+ return maxAgeSeconds;
+ }
+
+ public int getSMaxAgeSeconds() {
+ return sMaxAgeSeconds;
+ }
+
+ public boolean isPublic() {
+ return isPublic;
+ }
+
+ public boolean isMustRevalidate() {
+ return mustRevalidate;
+ }
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public Set<String> getVaryFields() {
+ return varyFields;
+ }
+
+ public String getContentEncoding() {
+ return contentEncoding;
+ }
+
+ public int getContentLength() {
+ return contentLength;
+ }
+
+ public String getConnection() {
+ return connection;
+ }
+
+ 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 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;
+ }
+
+ /**
+ * 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) {
+ // 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;
}
- /**
- * 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;
+ // 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;
}
- /**
- * 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;
- }
-
- /*
- * 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 (noStore) {
+ return false;
}
- /**
- * Returns true if a Vary header contains an asterisk. Such responses cannot
- * be cached.
- */
- public boolean hasVaryAll() {
- return varyFields.contains("*");
- }
+ return true;
+ }
- /**
- * Returns true if none of the Vary headers on this response have changed
- * between {@code cachedRequest} and {@code newRequest}.
- */
- public boolean varyMatches(Map<String, List<String>> cachedRequest,
- Map<String, List<String>> newRequest) {
- for (String field : varyFields) {
- if (!equal(cachedRequest.get(field), newRequest.get(field))) {
- return false;
- }
- }
- return true;
- }
+ /**
+ * Returns true if a Vary header contains an asterisk. Such responses cannot
+ * be cached.
+ */
+ public boolean hasVaryAll() {
+ return varyFields.contains("*");
+ }
- /**
- * 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;
- }
-
- /**
- * 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;
- }
-
+ /**
+ * Returns true if none of the Vary headers on this response have changed
+ * between {@code cachedRequest} and {@code newRequest}.
+ */
+ public boolean varyMatches(Map<String, List<String>> cachedRequest,
+ Map<String, List<String>> 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;
}
- /**
- * 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 < 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);
- }
- }
-
- 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);
+ if (request.isNoCache() || request.hasConditions()) {
+ return ResponseSource.NETWORK;
}
- /**
- * 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);
+ 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;
+ }
+
+ /**
+ * 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 < 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);
+ }
+ }
+
+ 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 a5c5842..325327d 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 ac4bb6c..798cff3 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.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.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.
*/
-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;
+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;
- 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<Proxy> proxySelectorProxies;
+ /* State for negotiating the next proxy to use. */
+ private boolean hasNextProxy;
+ private Proxy userSpecifiedProxy;
+ private Iterator<Proxy> 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<Proxy> 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<Proxy> 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;
+ 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();
}
-
- /** 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();
- }
- }
+ }
}
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 28a47ab..193beb6 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -22,64 +22,67 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CacheRequest;
+import java.net.URL;
import java.util.List;
public final class SpdyTransport implements Transport {
- private final HttpEngine httpEngine;
- private final SpdyConnection spdyConnection;
- private SpdyStream stream;
+ private final HttpEngine httpEngine;
+ private final SpdyConnection spdyConnection;
+ private SpdyStream stream;
- // TODO: set sentMillis
- // TODO: set cookie stuff
+ 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 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 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 writeRequestBody(RetryableOutputStream requestBody) throws IOException {
+ throw new UnsupportedOperationException();
+ }
- @Override public void writeRequestHeaders() throws IOException {
- if (stream != null) {
- return;
- }
- RawHeaders requestHeaders = httpEngine.requestHeaders.getHeaders();
- String version = httpEngine.connection.getHttpMinorVersion() == 1 ? "HTTP/1.1" : "HTTP/1.0";
- requestHeaders.addSpdyRequestHeaders(httpEngine.method, httpEngine.uri.getScheme(),
- HttpEngine.requestPath(httpEngine.policy.getURL()), version);
- boolean hasRequestBody = httpEngine.hasRequestBody();
- boolean hasResponseBody = true;
- stream = spdyConnection.newStream(requestHeaders.toNameValueBlock(),
- hasRequestBody, hasResponseBody);
- stream.setReadTimeout(httpEngine.policy.getReadTimeout());
- }
+ @Override public void flushRequest() throws IOException {
+ stream.getOutputStream().close();
+ }
- @Override public void writeRequestBody(RetryableOutputStream requestBody) throws IOException {
- throw new UnsupportedOperationException();
- }
+ @Override public ResponseHeaders readResponseHeaders() throws IOException {
+ List<String> nameValueBlock = stream.getResponseHeaders();
+ RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
+ rawHeaders.computeResponseStatusLineFromSpdyHeaders();
+ httpEngine.receiveHeaders(rawHeaders);
+ return new ResponseHeaders(httpEngine.uri, rawHeaders);
+ }
- @Override public void flushRequest() throws IOException {
- stream.getOutputStream().close();
- }
+ @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
+ return new UnknownLengthHttpInputStream(stream.getInputStream(), cacheRequest, httpEngine);
+ }
- @Override public ResponseHeaders readResponseHeaders() throws IOException {
- // TODO: fix the SPDY implementation so this throws a (buffered) IOException
- List<String> nameValueBlock = stream.getResponseHeaders();
- RawHeaders rawHeaders = RawHeaders.fromNameValueBlock(nameValueBlock);
- rawHeaders.computeResponseStatusLineFromSpdyHeaders();
- return new ResponseHeaders(httpEngine.uri, rawHeaders);
+ @Override public boolean makeReusable(boolean streamCancelled, OutputStream requestBodyOut,
+ InputStream responseBodyIn) {
+ if (streamCancelled) {
+ stream.closeLater(SpdyStream.RST_CANCEL);
}
-
- @Override public InputStream getTransferStream(CacheRequest cacheRequest) throws IOException {
- // TODO: handle HTTP responses that don't have a response body
- return stream.getInputStream();
- }
-
- @Override public boolean makeReusable(OutputStream requestBodyOut, InputStream responseBodyIn) {
- return true;
- }
+ 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 f1212d5..518827e 100644
--- a/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -22,50 +22,43 @@
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:
- * <ul>
- * <li><strong>Direct.</strong> 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.</li>
- * <li><strong>Buffered.</strong> 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.</li>
- * </ul>
- */
- // 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:
+ * <ul>
+ * <li><strong>Direct.</strong> 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.</li>
+ * <li><strong>Buffered.</strong> 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.</li>
+ * </ul>
+ */
+ // 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(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
new file mode 100644
index 0000000..729e0b9
--- /dev/null
+++ b/src/main/java/com/squareup/okhttp/internal/http/UnknownLengthHttpInputStream.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.CacheRequest;
+
+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;
+
+ 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 available() throws IOException {
+ checkNotClosed();
+ return in == null ? 0 : in.available();
+ }
+
+ @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 fc554f4..875fff0 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 @@
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 1fc3979..c585255 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 @@
* 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 f4136e6..774d791 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
@@ -16,156 +16,159 @@
package com.squareup.okhttp.internal.spdy;
final class Settings {
- /** Peer request to clear durable settings. */
- static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
+ /**
+ * 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;
- /** 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;
+ /** Peer request to clear durable settings. */
+ static final int FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS = 0x1;
- /** 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;
- /** Total number of settings. */
- static final int COUNT = 0x8;
+ /** 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;
- /** Bitfield of which flags that values. */
- private int set;
+ /** 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 flags that have {@link #PERSIST_VALUE}. */
- private int persistValue;
+ /** Bitfield of which flags that values. */
+ private int set;
- /** Bitfield of flags that have {@link #PERSISTED}. */
- private int persisted;
+ /** Bitfield of flags that have {@link #PERSIST_VALUE}. */
+ private int persistValue;
- /** Flag values. */
- private final int[] values = new int[COUNT];
+ /** Bitfield of flags that have {@link #PERSISTED}. */
+ private int persisted;
- void set(int id, int idFlags, int value) {
- if (id >= values.length) {
- return; // Discard unknown settings.
- }
+ /** Flag values. */
+ private final int[] values = new int[COUNT];
- 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;
+ }
- /**
- * 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 getDownloadRetransRate(int defaultValue) {
+ int bit = 1 << DOWNLOAD_RETRANS_RATE;
+ return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
+ }
- /**
- * Returns true if this setting was persisted.
- */
- boolean isPersisted(int id) {
- int bit = 1 << id;
- return (persisted & bit) != 0;
- }
+ int getInitialWindowSize(int defaultValue) {
+ int bit = 1 << INITIAL_WINDOW_SIZE;
+ return (bit & set) != 0 ? values[INITIAL_WINDOW_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));
- }
+ int getClientCertificateVectorSize(int defaultValue) {
+ int bit = 1 << CLIENT_CERTIFICATE_VECTOR_SIZE;
+ return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
+ }
+
+ /**
+ * Returns true if this user agent should use this setting in future SPDY
+ * connections to the same host.
+ */
+ 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 a67e3e8..4100b9e 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -21,7 +21,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.ProtocolException;
import java.net.Socket;
import java.util.HashMap;
import java.util.Iterator;
@@ -44,447 +43,541 @@
*/
public final class SpdyConnection implements Closeable {
- /*
- * Internal state of this connection is guarded by 'this'. No blocking
- * operations may be performed while holding this lock!
- *
- * Socket writes are guarded by spdyWriter.
- *
- * Socket reads are unguarded but are only made by the reader thread.
- *
- * Certain operations (like SYN_STREAM) need to synchronize on both the
- * spdyWriter (to do blocking I/O) and this (to create streams). Such
- * operations must synchronize on 'this' last. This ensures that we never
- * wait for a blocking operation while holding 'this'.
- */
+ // Internal state of this connection is guarded by 'this'. No blocking
+ // operations may be performed while holding this lock!
+ //
+ // Socket writes are guarded by spdyWriter.
+ //
+ // Socket reads are unguarded but are only made by the reader thread.
+ //
+ // Certain operations (like SYN_STREAM) need to synchronize on both the
+ // spdyWriter (to do blocking I/O) and this (to create streams). Such
+ // operations must synchronize on 'this' last. This ensures that we never
+ // 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 VERSION = 2;
+ 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;
+
+ /** 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;
+
+ private final SpdyReader spdyReader;
+ private final SpdyWriter spdyWriter;
+ private final ExecutorService readExecutor;
+ private final ExecutorService writeExecutor;
+ private final ExecutorService callbackExecutor;
+
+ private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
+ 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<Integer, Ping> pings;
+ private int nextPingId;
+
+ /** 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;
+
+ String prefix = builder.client ? "Spdy Client " : "Spdy Server ";
+ readExecutor =
+ new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(),
+ Util.newThreadFactory(prefix + "Reader", false));
+ writeExecutor =
+ new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
+ Util.newThreadFactory(prefix + "Writer", false));
+ callbackExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
+ new SynchronousQueue<Runnable>(), Util.newThreadFactory(prefix + "Callbacks", false));
+
+ 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;
+ }
+
+ private void setIdle(boolean value) {
+ idleStartTimeNs = value ? System.nanoTime() : 0L;
+ }
+
+ /** 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<String> 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<String> 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<Integer, Ping>();
+ 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;
+ try {
+ shutdown(shutdownStatusCode);
+ } catch (IOException e) {
+ thrown = e;
+ }
+
+ 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;
+ }
+ }
+
+ 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;
/**
- * True if this peer initiated the connection.
+ * @param client true if this peer initiated the connection; false if
+ * this peer accepted 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;
-
- private final SpdyReader spdyReader;
- private final SpdyWriter spdyWriter;
- private final ExecutorService readExecutor;
- private final ExecutorService writeExecutor;
- private final ExecutorService callbackExecutor;
-
- private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
- private int lastGoodStreamId;
- private int nextStreamId;
- private boolean shutdown;
-
- /** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */
- private Map<Integer, Ping> pings;
- private int nextPingId;
-
- /** 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;
-
- String prefix = builder.client ? "Spdy Client " : "Spdy Server ";
- readExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>(), Threads.newThreadFactory(prefix + "Reader", false));
- writeExecutor = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS,
- new LinkedBlockingQueue<Runnable>(), Threads.newThreadFactory(prefix + "Writer", false));
- callbackExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
- new SynchronousQueue<Runnable>(), Threads.newThreadFactory(prefix + "Callbacks", false));
-
- readExecutor.execute(new Reader());
+ public Builder(boolean client, Socket socket) throws IOException {
+ this(client, socket.getInputStream(), socket.getOutputStream());
}
/**
- * Returns the number of {@link SpdyStream#isOpen() open streams} on this
- * connection.
+ * @param client true if this peer initiated the connection; false if this
+ * peer accepted the connection.
*/
- public synchronized int openStreamCount() {
- return streams.size();
+ public Builder(boolean client, InputStream in, OutputStream out) {
+ this.client = client;
+ this.in = in;
+ this.out = out;
}
- private synchronized SpdyStream getStream(int id) {
- return streams.get(id);
+ public Builder handler(IncomingStreamHandler handler) {
+ this.handler = handler;
+ return this;
}
- synchronized SpdyStream removeStream(int streamId) {
- return streams.remove(streamId);
+ public SpdyConnection build() {
+ return new SpdyConnection(this);
}
+ }
- /**
- * 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<String> 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.
- SpdyStream stream;
- int streamId;
-
- synchronized (spdyWriter) {
- synchronized (this) {
- if (shutdown) {
- throw new IOException("shutdown");
- }
- streamId = nextStreamId;
- nextStreamId += 2;
- stream = new SpdyStream(streamId, this, requestHeaders, flags);
- if (stream.isOpen()) {
- streams.put(streamId, stream);
- }
- }
-
- spdyWriter.synStream(flags, streamId, associatedStreamId, priority, requestHeaders);
+ 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)) {
}
-
- return stream;
- }
-
- void writeSynReply(int streamId, int flags, List<String> 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);
+ 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) {
}
+ }
}
- void writeSynResetLater(final int streamId, final int statusCode) {
- writeExecutor.execute(new Runnable() {
- @Override public void run() {
- try {
- writeSynReset(streamId, statusCode);
- } 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();
+ }
}
- void writeSynReset(int streamId, int statusCode) throws IOException {
- spdyWriter.synReset(streamId, statusCode);
- }
-
- /**
- * 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<Integer, Ping>();
- pings.put(pingId, ping);
+ @Override
+ public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+ List<String> nameValueBlock) {
+ final SpdyStream synStream;
+ final SpdyStream previous;
+ synchronized (SpdyConnection.this) {
+ synStream =
+ new SpdyStream(streamId, SpdyConnection.this, flags, priority, slot, nameValueBlock,
+ settings);
+ if (shutdown) {
+ return;
}
- 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();
- }
- }
-
- /**
- * 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.
- */
- public void shutdown() throws IOException {
- synchronized (spdyWriter) {
- int lastGoodStreamId;
- synchronized (this) {
- if (shutdown) {
- return;
- }
- shutdown = true;
- lastGoodStreamId = this.lastGoodStreamId;
- }
- spdyWriter.goAway(0, lastGoodStreamId);
- }
- }
-
- /**
- * 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 {
- shutdown();
-
- SpdyStream[] streamsToClose = null;
- Ping[] pingsToCancel = null;
- synchronized (this) {
- if (!streams.isEmpty()) {
- streamsToClose = streams.values().toArray(new SpdyStream[streams.size()]);
- streams.clear();
- }
- if (pings != null) {
- pingsToCancel = pings.values().toArray(new Ping[pings.size()]);
- pings = null;
- }
- }
-
- if (streamsToClose != null) {
- for (SpdyStream stream : streamsToClose) {
- try {
- stream.close(SpdyStream.RST_CANCEL);
- } catch (Throwable ignored) {
- }
- }
- }
-
- if (pingsToCancel != null) {
- for (Ping ping : pingsToCancel) {
- ping.cancel();
- }
- }
-
- writeExecutor.shutdown();
- callbackExecutor.shutdown();
- readExecutor.shutdown();
- Util.closeAll(spdyReader, spdyWriter);
- }
-
- 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 {
+ 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 {
- while (spdyReader.nextFrame(this)) {
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- } finally {
- Util.closeQuietly(SpdyConnection.this);
- }
+ try {
+ handler.receive(synStream);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
}
-
- @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;
- }
- try {
- dataStream.receiveData(in, length);
- if ((flags & SpdyConnection.FLAG_FIN) != 0) {
- dataStream.receiveFin();
- }
- } catch (ProtocolException e) {
- Util.skipByReading(in, length);
- dataStream.closeLater(SpdyStream.RST_FLOW_CONTROL_ERROR);
- }
- }
-
- @Override public void synStream(int flags, int streamId, int associatedStreamId,
- int priority, List<String> nameValueBlock) {
- final SpdyStream synStream = new SpdyStream(streamId, SpdyConnection.this,
- nameValueBlock, flags);
- final SpdyStream previous;
- synchronized (SpdyConnection.this) {
- 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<String> nameValueBlock)
- throws IOException {
- SpdyStream replyStream = getStream(streamId);
- if (replyStream == null) {
- writeSynResetLater(streamId, SpdyStream.RST_INVALID_STREAM);
- return;
- }
- try {
- replyStream.receiveReply(nameValueBlock);
- if ((flags & SpdyConnection.FLAG_FIN) != 0) {
- replyStream.receiveFin();
- }
- } catch (ProtocolException e) {
- replyStream.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
- }
- }
-
- @Override public void headers(int flags, int streamId, List<String> nameValueBlock)
- throws IOException {
- SpdyStream replyStream = getStream(streamId);
- if (replyStream != null) {
- try {
- replyStream.receiveHeaders(nameValueBlock);
- } catch (ProtocolException e) {
- replyStream.closeLater(SpdyStream.RST_PROTOCOL_ERROR);
- }
- }
- }
-
- @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) {
- synchronized (SpdyConnection.this) {
- if (settings == null
- || (flags & Settings.FLAG_CLEAR_PREVIOUSLY_PERSISTED_SETTINGS) != 0) {
- settings = newSettings;
- } else {
- settings.merge(newSettings);
- }
- }
- }
-
- @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) {
- synchronized (SpdyConnection.this) {
- shutdown = true;
-
- // Fail all streams created after the last good stream ID.
- for (Iterator<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
- i.hasNext();) {
- Map.Entry<Integer, SpdyStream> 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 synReply(int flags, int streamId, List<String> 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<String> 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<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
+ i.hasNext(); ) {
+ Map.Entry<Integer, SpdyStream> 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 98fc975..db3b50c 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyReader.java
@@ -21,7 +21,7 @@
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
@@ -29,258 +29,286 @@
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
-/**
- * Read version 2 SPDY frames.
- */
+/** Read spdy/3 frames. */
final class SpdyReader implements Closeable {
- private static final String DICTIONARY_STRING = ""
- + "optionsgetheadpostputdeletetraceacceptaccept-charsetaccept-encodingaccept-"
- + "languageauthorizationexpectfromhostif-modified-sinceif-matchif-none-matchi"
- + "f-rangeif-unmodifiedsincemax-forwardsproxy-authorizationrangerefererteuser"
- + "-agent10010120020120220320420520630030130230330430530630740040140240340440"
- + "5406407408409410411412413414415416417500501502503504505accept-rangesageeta"
- + "glocationproxy-authenticatepublicretry-afterservervarywarningwww-authentic"
- + "ateallowcontent-basecontent-encodingcache-controlconnectiondatetrailertran"
- + "sfer-encodingupgradeviawarningcontent-languagecontent-lengthcontent-locati"
- + "oncontent-md5content-rangecontent-typeetagexpireslast-modifiedset-cookieMo"
- + "ndayTuesdayWednesdayThursdayFridaySaturdaySundayJanFebMarAprMayJunJulAugSe"
- + "pOctNovDecchunkedtext/htmlimage/pngimage/jpgimage/gifapplication/xmlapplic"
- + "ation/xhtmltext/plainpublicmax-agecharset=iso-8859-1utf-8gzipdeflateHTTP/1"
- + ".1statusversionurl\0";
- public static final byte[] DICTIONARY;
- static {
- try {
- DICTIONARY = DICTIONARY_STRING.getBytes("UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError(e);
+ static final byte[] DICTIONARY = ("\u0000\u0000\u0000\u0007options\u0000\u0000\u0000\u0004hea"
+ + "d\u0000\u0000\u0000\u0004post\u0000\u0000\u0000\u0003put\u0000\u0000\u0000\u0006dele"
+ + "te\u0000\u0000\u0000\u0005trace\u0000\u0000\u0000\u0006accept\u0000\u0000\u0000"
+ + "\u000Eaccept-charset\u0000\u0000\u0000\u000Faccept-encoding\u0000\u0000\u0000\u000Fa"
+ + "ccept-language\u0000\u0000\u0000\raccept-ranges\u0000\u0000\u0000\u0003age\u0000"
+ + "\u0000\u0000\u0005allow\u0000\u0000\u0000\rauthorization\u0000\u0000\u0000\rcache-co"
+ + "ntrol\u0000\u0000\u0000\nconnection\u0000\u0000\u0000\fcontent-base\u0000\u0000"
+ + "\u0000\u0010content-encoding\u0000\u0000\u0000\u0010content-language\u0000\u0000"
+ + "\u0000\u000Econtent-length\u0000\u0000\u0000\u0010content-location\u0000\u0000\u0000"
+ + "\u000Bcontent-md5\u0000\u0000\u0000\rcontent-range\u0000\u0000\u0000\fcontent-type"
+ + "\u0000\u0000\u0000\u0004date\u0000\u0000\u0000\u0004etag\u0000\u0000\u0000\u0006expe"
+ + "ct\u0000\u0000\u0000\u0007expires\u0000\u0000\u0000\u0004from\u0000\u0000\u0000"
+ + "\u0004host\u0000\u0000\u0000\bif-match\u0000\u0000\u0000\u0011if-modified-since"
+ + "\u0000\u0000\u0000\rif-none-match\u0000\u0000\u0000\bif-range\u0000\u0000\u0000"
+ + "\u0013if-unmodified-since\u0000\u0000\u0000\rlast-modified\u0000\u0000\u0000\blocati"
+ + "on\u0000\u0000\u0000\fmax-forwards\u0000\u0000\u0000\u0006pragma\u0000\u0000\u0000"
+ + "\u0012proxy-authenticate\u0000\u0000\u0000\u0013proxy-authorization\u0000\u0000"
+ + "\u0000\u0005range\u0000\u0000\u0000\u0007referer\u0000\u0000\u0000\u000Bretry-after"
+ + "\u0000\u0000\u0000\u0006server\u0000\u0000\u0000\u0002te\u0000\u0000\u0000\u0007trai"
+ + "ler\u0000\u0000\u0000\u0011transfer-encoding\u0000\u0000\u0000\u0007upgrade\u0000"
+ + "\u0000\u0000\nuser-agent\u0000\u0000\u0000\u0004vary\u0000\u0000\u0000\u0003via"
+ + "\u0000\u0000\u0000\u0007warning\u0000\u0000\u0000\u0010www-authenticate\u0000\u0000"
+ + "\u0000\u0006method\u0000\u0000\u0000\u0003get\u0000\u0000\u0000\u0006status\u0000"
+ + "\u0000\u0000\u0006200 OK\u0000\u0000\u0000\u0007version\u0000\u0000\u0000\bHTTP/1.1"
+ + "\u0000\u0000\u0000\u0003url\u0000\u0000\u0000\u0006public\u0000\u0000\u0000\nset-coo"
+ + "kie\u0000\u0000\u0000\nkeep-alive\u0000\u0000\u0000\u0006origin100101201202205206300"
+ + "302303304305306307402405406407408409410411412413414415416417502504505203 Non-Authori"
+ + "tative Information204 No Content301 Moved Permanently400 Bad Request401 Unauthorized"
+ + "403 Forbidden404 Not Found500 Internal Server Error501 Not Implemented503 Service Un"
+ + "availableJan Feb Mar Apr May Jun Jul Aug Sept Oct Nov Dec 00:00:00 Mon, Tue, Wed, Th"
+ + "u, Fri, Sat, Sun, GMTchunked,text/html,image/png,image/jpg,image/gif,application/xml"
+ + ",application/xhtml+xml,text/plain,text/javascript,publicprivatemax-age=gzip,deflate,"
+ + "sdchcharset=utf-8charset=iso-8859-1,utf-,*,enq=0.").getBytes(Util.UTF_8);
+
+ private final DataInputStream in;
+ private final DataInputStream nameValueBlockIn;
+ private int compressedLimit;
+
+ SpdyReader(InputStream in) {
+ this.in = new DataInputStream(in);
+ this.nameValueBlockIn = newNameValueBlockStream();
+ }
+
+ /**
+ * Send the next frame to {@code handler}. Returns true unless there are no
+ * more frames on the stream.
+ */
+ public boolean nextFrame(Handler handler) throws IOException {
+ int w1;
+ try {
+ w1 = in.readInt();
+ } catch (IOException e) {
+ return false; // This might be a normal socket close.
+ }
+ int w2 = in.readInt();
+
+ boolean control = (w1 & 0x80000000) != 0;
+ int flags = (w2 & 0xff000000) >>> 24;
+ int length = (w2 & 0xffffff);
+
+ if (control) {
+ int version = (w1 & 0x7fff0000) >>> 16;
+ int type = (w1 & 0xffff);
+
+ if (version != 3) {
+ throw new ProtocolException("version != 3: " + version);
+ }
+
+ switch (type) {
+ case SpdyConnection.TYPE_SYN_STREAM:
+ readSynStream(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_SYN_REPLY:
+ readSynReply(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_RST_STREAM:
+ readRstStream(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_SETTINGS:
+ readSettings(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_NOOP:
+ if (length != 0) throw ioException("TYPE_NOOP length: %d != 0", length);
+ handler.noop();
+ return true;
+
+ case SpdyConnection.TYPE_PING:
+ readPing(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_GOAWAY:
+ readGoAway(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_HEADERS:
+ readHeaders(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_WINDOW_UPDATE:
+ readWindowUpdate(handler, flags, length);
+ return true;
+
+ case SpdyConnection.TYPE_CREDENTIAL:
+ Util.skipByReading(in, length);
+ throw new UnsupportedOperationException("TODO"); // TODO: implement
+
+ default:
+ throw new IOException("Unexpected frame");
+ }
+ } else {
+ int streamId = w1 & 0x7fffffff;
+ handler.data(flags, streamId, in, length);
+ return true;
+ }
+ }
+
+ private void readSynStream(Handler handler, int flags, int length) throws IOException {
+ int w1 = in.readInt();
+ int w2 = in.readInt();
+ int s3 = in.readShort();
+ int streamId = w1 & 0x7fffffff;
+ int associatedStreamId = w2 & 0x7fffffff;
+ int priority = (s3 & 0xe000) >>> 13;
+ int slot = s3 & 0xff;
+ List<String> 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<String> 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<String> nameValueBlock = readNameValueBlock(length - 4);
+ handler.headers(flags, streamId, nameValueBlock);
+ }
+
+ private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
+ if (length != 8) throw ioException("TYPE_WINDOW_UPDATE length: %d != 8", length);
+ int w1 = in.readInt();
+ int w2 = in.readInt();
+ int streamId = w1 & 0x7fffffff;
+ int deltaWindowSize = w2 & 0x7fffffff;
+ handler.windowUpdate(flags, streamId, deltaWindowSize);
+ }
+
+ private DataInputStream newNameValueBlockStream() {
+ // Limit the inflater input stream to only those bytes in the Name/Value block.
+ final InputStream throttleStream = new InputStream() {
+ @Override public int read() throws IOException {
+ return Util.readSingleByte(this);
+ }
+
+ @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
+ byteCount = Math.min(byteCount, compressedLimit);
+ int consumed = in.read(buffer, offset, byteCount);
+ compressedLimit -= consumed;
+ return consumed;
+ }
+
+ @Override public void close() throws IOException {
+ in.close();
+ }
+ };
+
+ // Subclass inflater to install a dictionary when it's needed.
+ Inflater inflater = new Inflater() {
+ @Override
+ public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
+ int result = super.inflate(buffer, offset, count);
+ if (result == 0 && needsDictionary()) {
+ setDictionary(DICTIONARY);
+ result = super.inflate(buffer, offset, count);
}
+ return result;
+ }
+ };
+
+ return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
+ }
+
+ private List<String> readNameValueBlock(int length) throws IOException {
+ this.compressedLimit += length;
+ try {
+ int numberOfPairs = nameValueBlockIn.readInt();
+ List<String> entries = new ArrayList<String>(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 final DataInputStream in;
- private final DataInputStream nameValueBlockIn;
- private int compressedLimit;
+ 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");
+ }
- SpdyReader(InputStream in) {
- this.in = new DataInputStream(in);
- this.nameValueBlockIn = newNameValueBlockStream();
+ 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);
}
-
- /**
- * Send the next frame to {@code handler}. Returns true unless there are no
- * more frames on the stream.
- */
- public boolean nextFrame(Handler handler) throws IOException {
- int w1;
- try {
- w1 = in.readInt();
- } catch (IOException e) {
- return false; // This might be a normal socket close.
- }
- int w2 = in.readInt();
-
- boolean control = (w1 & 0x80000000) != 0;
- int flags = (w2 & 0xff000000) >>> 24;
- int length = (w2 & 0xffffff);
-
- if (control) {
- int version = (w1 & 0x7fff0000) >>> 16;
- int type = (w1 & 0xffff);
-
- 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;
-
- default:
- throw new IOException("Unexpected frame");
- }
- } else {
- int streamId = w1 & 0x7fffffff;
- handler.data(flags, streamId, in, length);
- return true;
- }
+ 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 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 & 0xc000 >>> 14;
- // int unused = s3 & 0x3fff;
- List<String> nameValueBlock = readNameValueBlock(length - 10);
- handler.synStream(flags, streamId, associatedStreamId, priority, nameValueBlock);
- }
+ private static IOException ioException(String message, Object... args) throws IOException {
+ throw new IOException(String.format(message, args));
+ }
- private void readSynReply(Handler handler, int flags, int length) throws IOException {
- int w1 = in.readInt();
- in.readShort(); // unused
- int streamId = w1 & 0x7fffffff;
- List<String> nameValueBlock = readNameValueBlock(length - 6);
- handler.synReply(flags, streamId, nameValueBlock);
- }
+ @Override public void close() throws IOException {
+ Util.closeAll(in, nameValueBlockIn);
+ }
- 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);
- }
+ public interface Handler {
+ void data(int flags, int streamId, InputStream in, int length) throws IOException;
- private void readHeaders(Handler handler, int flags, int length) throws IOException {
- int w1 = in.readInt();
- in.readShort(); // unused
- int streamId = w1 & 0x7fffffff;
- List<String> nameValueBlock = readNameValueBlock(length - 6);
- handler.headers(flags, streamId, nameValueBlock);
- }
+ void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+ List<String> nameValueBlock);
- private DataInputStream newNameValueBlockStream() {
- // Limit the inflater input stream to only those bytes in the Name/Value block.
- final InputStream throttleStream = new InputStream() {
- @Override public int read() throws IOException {
- return Util.readSingleByte(this);
- }
-
- @Override public int read(byte[] buffer, int offset, int byteCount) throws IOException {
- byteCount = Math.min(byteCount, compressedLimit);
- int consumed = in.read(buffer, offset, byteCount);
- compressedLimit -= consumed;
- return consumed;
- }
-
- @Override public void close() throws IOException {
- in.close();
- }
- };
-
- // Subclass inflater to install a dictionary when it's needed.
- Inflater inflater = new Inflater() {
- @Override
- public int inflate(byte[] buffer, int offset, int count) throws DataFormatException {
- int result = super.inflate(buffer, offset, count);
- if (result == 0 && needsDictionary()) {
- setDictionary(DICTIONARY);
- result = super.inflate(buffer, offset, count);
- }
- return result;
- }
- };
-
- return new DataInputStream(new InflaterInputStream(throttleStream, inflater));
- }
-
- private List<String> readNameValueBlock(int length) throws IOException {
- this.compressedLimit += length;
- try {
- int numberOfPairs = nameValueBlockIn.readShort();
- List<String> entries = new ArrayList<String>(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.readShort();
- 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 != 4) throw ioException("TYPE_GOAWAY length: %d != 4", length);
- int lastGoodStreamId = in.readInt() & 0x7fffffff;
- handler.goAway(flags, lastGoodStreamId);
- }
-
- 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();
- // The ID is a 24 bit little-endian value, so 0xabcdefxx becomes 0x00efcdab.
- int id = ((w1 & 0xff000000) >>> 24)
- | ((w1 & 0xff0000) >>> 8)
- | ((w1 & 0xff00) << 8);
- int idFlags = (w1 & 0xff);
- 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,
- List<String> nameValueBlock);
- void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
- void headers(int flags, int streamId, List<String> nameValueBlock) throws IOException;
- void rstStream(int flags, int streamId, int statusCode);
- void settings(int flags, Settings settings);
- void noop();
- void ping(int flags, int streamId);
- void goAway(int flags, int lastGoodStreamId);
- }
+ void synReply(int flags, int streamId, List<String> nameValueBlock) throws IOException;
+ void headers(int flags, int streamId, List<String> 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 5c3d971..744a04e 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -17,603 +17,717 @@
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.ProtocolException;
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 {
- /*
- * Internal state is guarded by this. No long-running or potentially
- * blocking operations are performed while the lock is held.
- */
+ // Internal state is guarded by this. No long-running or potentially
+ // 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",
- };
+ 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_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;
- private final int id;
- private final SpdyConnection connection;
- private long readTimeoutMillis = 0;
+ /**
+ * 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;
- /** Headers sent by the stream initiator. Immutable and non null. */
- private final List<String> requestHeaders;
+ 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 in the stream reply. Null if reply is either not sent or not sent yet. */
- private List<String> responseHeaders;
+ /** Headers sent by the stream initiator. Immutable and non null. */
+ private final List<String> requestHeaders;
- private final SpdyDataInputStream in = new SpdyDataInputStream();
- private final SpdyDataOutputStream out = new SpdyDataOutputStream();
+ /** Headers sent in the stream reply. Null if reply is either not sent or not sent yet. */
+ private List<String> responseHeaders;
+
+ 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;
+
+ SpdyStream(int id, SpdyConnection connection, int flags, int priority, int slot,
+ List<String> 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);
+ }
+
+ /**
+ * Returns true if this stream is open. A stream is open until either:
+ * <ul>
+ * <li>A {@code SYN_RESET} frame abnormally terminates the stream.
+ * <li>Both input and output streams have transmitted all data and
+ * headers.
+ * </ul>
+ * 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;
+ }
+
+ /** 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<String> getRequestHeaders() {
+ return requestHeaders;
+ }
+
+ /**
+ * Returns the stream's response headers, blocking if necessary if they
+ * have not been received yet.
+ */
+ public synchronized List<String> 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<String> 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);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * 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<String> 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<String> headers) throws IOException {
+ assert (!Thread.holdsLock(SpdyStream.this));
+ boolean protocolError = false;
+ synchronized (this) {
+ if (responseHeaders != null) {
+ List<String> newHeaders = new ArrayList<String>();
+ 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);
+ 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
+ // is the first byte to write.
+ //
+ // { - - - X X X X - - - }
+ // ^ ^
+ // pos limit
+ //
+ // { X X X - - - - X X X }
+ // ^ ^
+ // limit pos
+
+ 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 last byte to be read. Never buffer.length */
+ private int limit;
+
+ /** True if the caller has closed this stream. */
+ private boolean closed;
/**
- * 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.
+ * True if either side has cleanly shut down this stream. We will
+ * receive no more bytes beyond those already in the buffer.
*/
- private int rstStatusCode = -1;
+ private boolean finished;
- SpdyStream(int id, SpdyConnection connection, List<String> requestHeaders, int flags) {
- this.id = id;
- this.connection = connection;
- this.requestHeaders = requestHeaders;
+ /**
+ * 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;
- if (isLocallyInitiated()) {
- // I am the sender
- in.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
- out.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
+ @Override public int available() throws IOException {
+ synchronized (SpdyStream.this) {
+ checkNotClosed();
+ if (pos == -1) {
+ return 0;
+ } else if (limit > pos) {
+ return limit - pos;
} else {
- // I am the receiver
- in.finished = (flags & SpdyConnection.FLAG_FIN) != 0;
- out.finished = (flags & SpdyConnection.FLAG_UNIDIRECTIONAL) != 0;
+ 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 true if this stream is open. A stream is open until either:
- * <ul>
- * <li>A {@code SYN_RESET} frame abnormally terminates the stream.
- * <li>Both input and output streams have transmitted all data.
- * </ul>
- * 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.
+ * Returns once the input stream is either readable or finished. Throws
+ * a {@link SocketTimeoutException} if the read timeout elapses before
+ * that happens.
*/
- public synchronized boolean isOpen() {
- if (rstStatusCode != -1) {
- return false;
+ 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();
+ }
}
- if ((in.finished || in.closed) && (out.finished || out.closed)) {
- return false;
+ } 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;
}
- return true;
+ }
+ 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 true if this stream was created by this peer.
+ * 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.
*/
- public boolean isLocallyInitiated() {
- boolean streamIsClient = (id % 2 == 1);
- return connection.client == streamIsClient;
- }
+ private void waitUntilWritable(int count, boolean last) throws IOException {
+ try {
+ while (unacknowledgedBytes + count >= writeWindowSize) {
+ SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
- public SpdyConnection getConnection() {
- return connection;
- }
-
- public List<String> getRequestHeaders() {
- return requestHeaders;
- }
-
- /**
- * Returns the stream's response headers, blocking if necessary if they
- * have not been received yet.
- */
- public synchronized List<String> getResponseHeaders() throws IOException {
- try {
- while (responseHeaders == null && rstStatusCode == -1) {
- wait();
- }
- if (responseHeaders != null) {
- return responseHeaders;
- }
+ // 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) {
- InterruptedIOException rethrow = new InterruptedIOException();
- rethrow.initCause(e);
- throw rethrow;
+ }
}
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException();
+ }
}
- /**
- * 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;
+ 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());
+ }
+ }
}
-
- /**
- * 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<String> 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);
- }
-
- /**
- * 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;
- }
-
- /**
- * Abnormally terminate this stream.
- */
- public void close(int rstStatusCode) throws IOException {
- if (!closeInternal(rstStatusCode)) {
- return; // Already closed.
- }
- connection.writeSynReset(id, rstStatusCode);
- }
-
- 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<String> strings) throws IOException {
- assert (!Thread.holdsLock(SpdyStream.this));
- synchronized (this) {
- if (!isLocallyInitiated() || responseHeaders != null) {
- throw new ProtocolException();
- }
- responseHeaders = strings;
- notifyAll();
- }
- }
-
- void receiveHeaders(List<String> headers) throws IOException {
- assert (!Thread.holdsLock(SpdyStream.this));
- synchronized (this) {
- if (responseHeaders == null) {
- throw new ProtocolException();
- }
- List<String> newHeaders = new ArrayList<String>();
- newHeaders.addAll(responseHeaders);
- newHeaders.addAll(headers);
- this.responseHeaders = newHeaders;
- }
- }
-
- 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 String rstStatusString() {
- return rstStatusCode > 0 && rstStatusCode < STATUS_CODE_NAMES.length
- ? STATUS_CODE_NAMES[rstStatusCode]
- : Integer.toString(rstStatusCode);
- }
-
- /**
- * 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
- * is the first byte to write.
- *
- * { - - - X X X X - - - }
- * ^ ^
- * pos limit
- *
- * { X X X - - - - X X X }
- * ^ ^
- * limit pos
- */
-
- private final byte[] buffer = new byte[64 * 1024]; // 64KiB specified by TODO
-
- /** 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;
-
- /** 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;
-
- @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;
- }
-
- // TODO: notify peer of flow-control
-
- 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;
- synchronized (SpdyStream.this) {
- finished = this.finished;
- pos = this.pos;
- firstNewByte = this.limit;
- limit = this.limit;
- if (byteCount > buffer.length - available()) {
- throw new ProtocolException();
- }
- }
-
- // 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;
-
- @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 flags = 0;
- if (last) {
- flags |= SpdyConnection.FLAG_FIN;
- }
- int length = pos - DATA_FRAME_HEADER_LENGTH;
- 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;
- }
-
- 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 8cd6ae0..b3d1d1f 100644
--- a/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java
+++ b/src/main/java/com/squareup/okhttp/internal/spdy/SpdyWriter.java
@@ -26,148 +26,151 @@
import java.util.List;
import java.util.zip.Deflater;
-/**
- * Write version 2 SPDY 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<String> 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<String> 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<String> 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<String> 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, List<String> 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 & 0x3) << 30 | (unused & 0x3FFF) << 16);
- 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<String> nameValueBlock) throws IOException {
- writeNameValueBlockToBuffer(nameValueBlock);
- int type = SpdyConnection.TYPE_SYN_REPLY;
- int length = nameValueBlockBuffer.size() + 6;
- int unused = 0;
+ 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);
- out.writeShort(unused);
- 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<String> nameValueBlock) throws IOException {
- writeNameValueBlockToBuffer(nameValueBlock);
- int type = SpdyConnection.TYPE_HEADERS;
- int length = nameValueBlockBuffer.size() + 6;
- int unused = 0;
+ 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);
- out.writeShort(unused);
- 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 synReset(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<String> nameValueBlock) throws IOException {
- nameValueBlockBuffer.reset();
- int numberOfPairs = nameValueBlock.size() / 2;
- nameValueBlockOut.writeShort(numberOfPairs);
- for (String s : nameValueBlock) {
- nameValueBlockOut.writeShort(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);
- // settingId 0x00efcdab and settingFlags 0x12 combine to 0xabcdef12.
- out.writeInt(((i & 0xff0000) >>> 8)
- | ((i & 0xff00) << 8)
- | ((i & 0xff) << 24)
- | (settingsFlags & 0xff));
- 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) throws IOException {
- int type = SpdyConnection.TYPE_GOAWAY;
- int length = 4;
- out.writeInt(0x80000000 | (SpdyConnection.VERSION & 0x7fff) << 16 | type & 0xffff);
- out.writeInt((flags & 0xff) << 24 | length & 0xffffff);
- out.writeInt(lastGoodStreamId);
- 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/main/java/com/squareup/okhttp/internal/spdy/Threads.java b/src/main/java/com/squareup/okhttp/internal/spdy/Threads.java
deleted file mode 100644
index ef1d4f3..0000000
--- a/src/main/java/com/squareup/okhttp/internal/spdy/Threads.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp.internal.spdy;
-
-import java.util.concurrent.ThreadFactory;
-
-final class Threads {
- 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;
- }
- };
- }
-
- private Threads() {
- }
-}
diff --git a/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
new file mode 100644
index 0000000..dca9625
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.google.mockwebserver.MockWebServer;
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import javax.net.ssl.SSLContext;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+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();
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final MockSpdyServer spdyServer = new MockSpdyServer(sslContext.getSocketFactory());
+ private InetSocketAddress spdySocketAddress;
+ private Address spdyAddress;
+
+ private final MockWebServer httpServer = new MockWebServer();
+ private Address httpAddress;
+ private InetSocketAddress httpSocketAddress;
+
+ private Connection httpA;
+ private Connection httpB;
+ private Connection httpC;
+ private Connection httpD;
+ private Connection httpE;
+ private Connection spdyA;
+ private Connection spdyB;
+
+ @Before public void setUp() throws Exception {
+ httpServer.play();
+ httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), null, null, null);
+ httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
+ httpServer.getPort());
+
+ spdyServer.play();
+ spdyAddress =
+ new Address(spdyServer.getHostName(), spdyServer.getPort(), sslContext.getSocketFactory(),
+ new RecordingHostnameVerifier(), null);
+ spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
+ spdyServer.getPort());
+
+ httpA = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpA.connect(100, 100, null);
+ httpB = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpB.connect(100, 100, null);
+ httpC = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpC.connect(100, 100, null);
+ httpD = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpD.connect(100, 100, null);
+ httpE = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ httpE.connect(100, 100, null);
+ spdyA = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+ spdyA.connect(100, 100, null);
+ spdyB = new Connection(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+ spdyB.connect(100, 100, null);
+ }
+
+ @After public void tearDown() throws Exception {
+ httpServer.shutdown();
+ spdyServer.shutdown();
+
+ Util.closeQuietly(httpA);
+ Util.closeQuietly(httpB);
+ Util.closeQuietly(httpC);
+ Util.closeQuietly(httpD);
+ Util.closeQuietly(httpE);
+ Util.closeQuietly(spdyA);
+ Util.closeQuietly(spdyB);
+ }
+
+ @Test public void poolSingleHttpConnection() throws IOException {
+ ConnectionPool pool = new ConnectionPool(1, KEEP_ALIVE_DURATION_MS);
+ Connection connection = pool.get(httpAddress);
+ assertNull(connection);
+
+ connection = new Connection(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
+ connection.connect(100, 100, null);
+ assertEquals(0, pool.getConnectionCount());
+ pool.recycle(connection);
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(0, pool.getSpdyConnectionCount());
+
+ Connection recycledConnection = pool.get(httpAddress);
+ assertEquals(connection, recycledConnection);
+ assertTrue(recycledConnection.isAlive());
+
+ recycledConnection = pool.get(httpAddress);
+ assertNull(recycledConnection);
+ }
+
+ @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ pool.recycle(httpB);
+ pool.recycle(httpC);
+ assertPooled(pool, httpC, httpB);
+ }
+
+ @Test public void getSpdyConnection() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.maybeShare(spdyA);
+ assertSame(spdyA, pool.get(spdyAddress));
+ assertPooled(pool, spdyA);
+ }
+
+ @Test public void getHttpConnection() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ assertSame(httpA, pool.get(httpAddress));
+ assertPooled(pool);
+ }
+
+ @Test public void idleConnectionNotReturned() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
+ assertNull(pool.get(httpAddress));
+ assertPooled(pool);
+ }
+
+ @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ pool.recycle(httpB);
+ pool.recycle(httpC);
+ pool.recycle(httpD);
+ assertPooled(pool, httpD, httpC);
+ }
+
+ @Test public void expiredConnectionsAreEvicted() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ pool.recycle(httpB);
+ Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
+ pool.get(spdyAddress); // Force the cleanup callable to run.
+ assertPooled(pool);
+ }
+
+ @Test public void nonAliveConnectionNotReturned() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ httpA.close();
+ assertNull(pool.get(httpAddress));
+ assertPooled(pool);
+ }
+
+ @Test public void differentAddressConnectionNotReturned() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ assertNull(pool.get(spdyAddress));
+ assertPooled(pool, httpA);
+ }
+
+ @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.maybeShare(spdyA);
+ pool.recycle(httpA);
+ assertPooled(pool, httpA, spdyA);
+ assertSame(spdyA, pool.get(spdyAddress));
+ assertPooled(pool, spdyA, httpA);
+ }
+
+ @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(httpA);
+ pool.recycle(httpB);
+ assertSame(httpA, pool.get(httpAddress));
+ }
+
+ @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ httpA.getSocket().shutdownInput();
+ pool.recycle(httpA); // Should close httpA.
+ assertTrue(httpA.getSocket().isClosed());
+ }
+
+ @Test public void shareHttpConnectionDoesNothing() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.maybeShare(httpA);
+ assertPooled(pool);
+ }
+
+ @Test public void recycleSpdyConnectionDoesNothing() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ pool.recycle(spdyA);
+ assertPooled(pool);
+ }
+
+ @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));
+ assertNull(pool.get(httpAddress));
+ assertPooled(pool, spdyA); // Connection should still be in the pool.
+ Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
+ assertNull(pool.get(httpAddress));
+ assertPooled(pool);
+ }
+
+ @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));
+ assertNull(pool.get(spdyAddress));
+ assertPooled(pool, httpA); // Connection should still be in the pool.
+ Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
+ assertNull(pool.get(spdyAddress));
+ assertPooled(pool);
+ }
+
+ @Test public void maxConnections() throws IOException, InterruptedException {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+
+ // Pool should be empty.
+ assertEquals(0, pool.getConnectionCount());
+
+ // http A should be added to the pool.
+ pool.recycle(httpA);
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(0, pool.getSpdyConnectionCount());
+
+ // http B should be added to the pool.
+ pool.recycle(httpB);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
+ assertEquals(0, pool.getSpdyConnectionCount());
+
+ // http C should be added and http A should be removed.
+ pool.recycle(httpC);
+ Thread.sleep(50);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
+ assertEquals(0, pool.getSpdyConnectionCount());
+
+ // spdy A should be added and http B should be removed.
+ pool.maybeShare(spdyA);
+ Thread.sleep(50);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // http C should be removed from the pool.
+ Connection recycledHttpConnection = pool.get(httpAddress);
+ assertNotNull(recycledHttpConnection);
+ assertTrue(recycledHttpConnection.isAlive());
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(0, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // spdy A will be returned and kept in the pool.
+ Connection sharedSpdyConnection = pool.get(spdyAddress);
+ assertNotNull(sharedSpdyConnection);
+ assertEquals(spdyA, sharedSpdyConnection);
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(0, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // Nothing should change.
+ pool.recycle(httpC);
+ Thread.sleep(50);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // Nothing should change.
+ pool.maybeShare(spdyB);
+ Thread.sleep(50);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // An http connection should be removed from the pool.
+ recycledHttpConnection = pool.get(httpAddress);
+ assertNotNull(recycledHttpConnection);
+ assertTrue(recycledHttpConnection.isAlive());
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(0, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // Shouldn't change numbers because spdyConnections A and B user the same server address.
+ pool.maybeShare(spdyB);
+ Thread.sleep(50);
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(0, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // spdy A will be returned and kept in the pool. Pool shouldn't change.
+ sharedSpdyConnection = pool.get(spdyAddress);
+ assertEquals(spdyA, sharedSpdyConnection);
+ assertNotNull(sharedSpdyConnection);
+ assertEquals(1, pool.getConnectionCount());
+ assertEquals(0, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // http D should be added to the pool.
+ pool.recycle(httpD);
+ Thread.sleep(50);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // http E should be added to the pool. spdy A should be removed from the pool.
+ pool.recycle(httpE);
+ Thread.sleep(50);
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
+ assertEquals(0, pool.getSpdyConnectionCount());
+ }
+
+ @Test public void connectionCleanup() throws IOException, InterruptedException {
+ ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+
+ // Add 3 connections to the pool.
+ pool.recycle(httpA);
+ pool.recycle(httpB);
+ pool.maybeShare(spdyA);
+ assertEquals(3, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ // Kill http A.
+ Util.closeQuietly(httpA);
+
+ // Force pool to run a clean up.
+ assertNotNull(pool.get(spdyAddress));
+ Thread.sleep(50);
+
+ assertEquals(2, pool.getConnectionCount());
+ assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getSpdyConnectionCount());
+
+ Thread.sleep(KEEP_ALIVE_DURATION_MS);
+ // Force pool to run a clean up.
+ assertNull(pool.get(httpAddress));
+ assertNull(pool.get(spdyAddress));
+
+ Thread.sleep(50);
+
+ assertEquals(0, pool.getConnectionCount());
+ assertEquals(0, pool.getHttpConnectionCount());
+ assertEquals(0, pool.getSpdyConnectionCount());
+ }
+
+ 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 160ec78..72cc70d 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.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<String> expectedLines = new ArrayList<String>();
+ 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<String> readJournalLines() throws Exception {
+ List<String> result = new ArrayList<String>();
+ 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);
+ 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());
+ }
- @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<String> expectedLines = new ArrayList<String>();
- 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<String> readJournalLines() throws Exception {
- List<String> result = new ArrayList<String>();
- 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 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
new file mode 100644
index 0000000..9eff919
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import java.net.Authenticator;
+import java.net.PasswordAuthentication;
+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=";
+
+ public final List<String> calls = new ArrayList<String>();
+ public final PasswordAuthentication authentication;
+
+ public RecordingAuthenticator(PasswordAuthentication authentication) {
+ this.authentication = authentication;
+ }
+
+ 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;
+ }
+}
diff --git a/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java b/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
new file mode 100644
index 0000000..b3e2369
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+public final class RecordingHostnameVerifier implements HostnameVerifier {
+ public final List<String> calls = new ArrayList<String>();
+
+ 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 c0a520c..0677263 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 @@
* reuse SSL context instances where possible.
*/
public final class SslContextBuilder {
- static {
- Security.addProvider(new BouncyCastleProvider());
+ 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);
}
-
- 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 5f85b52..252f6ac 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 f93c493..11d7239 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.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 841647d..e54ed21 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 @@
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,1723 @@
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.<String, List<String>>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<String, List<String>> 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;
+ }
+ });
+
+ URL url = server.getUrl("/");
+ HttpURLConnection connection = openConnection(url);
+ assertEquals(body, readAscii(connection));
+ assertEquals(1, cacheCount.get());
+ }
+
+ @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
+ testResponseCaching(TransferKind.FIXED_LENGTH);
+ }
+
+ @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
+ testResponseCaching(TransferKind.CHUNKED);
+ }
+
+ @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
+ testResponseCaching(TransferKind.END_OF_STREAM);
+ }
+
+ /**
+ * 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();
+
+ // 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());
+
+ 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());
+
+ assertEquals(-1, in.read());
+ in.close();
+ assertEquals(1, cache.getWriteSuccessCount());
+ assertEquals(0, cache.getWriteAbortCount());
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getHitCount());
+ }
+
+ @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));
+
+ // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
+ String suite = connection.getCipherSuite();
+ List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates());
+ List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates());
+ Principal peerPrincipal = connection.getPeerPrincipal();
+ Principal localPrincipal = connection.getLocalPrincipal();
+
+ connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
+ connection.setSSLSocketFactory(sslContext.getSocketFactory());
+ connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+ assertEquals("ABC", readAscii(connection));
+
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(1, 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 cacheReturnsInsecureResponseForSecureRequest() throws IOException {
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setBody("ABC"));
+ server.enqueue(new MockResponse().setBody("DEF"));
+ server.play();
+
+ ResponseCache.setDefault(new InsecureResponseCache());
+
+ HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
+ connection1.setSSLSocketFactory(sslContext.getSocketFactory());
+ connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+ assertEquals("ABC", readAscii(connection1));
+
+ // 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 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();
+
+ HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("ABC", readAscii(connection));
+
+ connection = openConnection(server.getUrl("/")); // cached!
+ assertEquals("ABC", readAscii(connection));
+
+ assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects
+ assertEquals(2, cache.getNetworkCount());
+ assertEquals(2, cache.getHitCount());
+ }
+
+ @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();
+
+ assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo"))));
+ RecordedRequest request1 = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", request1.getRequestLine());
+ assertEquals(0, request1.getSequenceNumber());
+
+ assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar"))));
+ RecordedRequest request2 = server.takeRequest();
+ assertEquals("GET /bar HTTP/1.1", request2.getRequestLine());
+ assertEquals(1, request2.getSequenceNumber());
+
+ // 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 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<Map<String, List<String>>> requestHeadersRef =
+ new AtomicReference<Map<String, List<String>>>();
+ ResponseCache.setDefault(new ResponseCache() {
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> 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 {
+ // 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();
}
- @Test public void responseCaching_410() throws Exception {
- // the HTTP spec permits caching 410s, but the RI doesn't.
- assertCached(true, 410);
+ 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 clientPrematureDisconnectWithContentLengthHeader() throws IOException {
+ testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
+ }
+
+ @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) {
}
- 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();
+ 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());
+ }
- URL url = server.getUrl("/");
- HttpURLConnection conn = openConnection(url);
- assertEquals(responseCode, conn.getResponseCode());
+ @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();
- // exhaust the content stream
- readAscii(conn);
+ URL url = server.getUrl("/");
+ assertEquals("A", readAscii(openConnection(url)));
+ URLConnection connection = openConnection(url);
+ assertEquals("A", readAscii(connection));
+ assertNull(connection.getHeaderField("Warning"));
+ }
- CacheResponse cached = cache.get(url.toURI(), "GET",
- Collections.<String, List<String>>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 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<String> 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();
+
+ 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 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("/?foo=bar");
+ assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("B", readAscii(openConnection(url)));
+ }
+
+ @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<String> 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<String> headers = conditionalRequest.getHeaders();
+ assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ }
+
+ @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"));
+ }
+
+ @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 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 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 requestMethodOptionsIsNotCached() throws Exception {
+ testRequestMethod("OPTIONS", false);
+ }
+
+ @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 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 requestMethodDeleteIsNotCached() throws Exception {
+ testRequestMethod("DELETE", false);
+ }
+
+ @Test public void requestMethodTraceIsNotCached() throws Exception {
+ testRequestMethod("TRACE", false);
+ }
+
+ 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();
+
+ URL url = server.getUrl("/");
+
+ 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"));
}
+ }
- /**
- * 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();
+ @Test public void postInvalidatesCache() throws Exception {
+ testMethodInvalidates("POST");
+ }
- server.enqueue(new MockResponse()
- .setStatus("HTTP/1.1 200 Fantastic")
- .addHeader("fgh: ijk")
- .setBody(body));
- server.play();
+ @Test public void putInvalidatesCache() throws Exception {
+ testMethodInvalidates("PUT");
+ }
- ResponseCache.setDefault(new ResponseCache() {
- @Override public CacheResponse get(URI uri, String requestMethod,
- Map<String, List<String>> 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;
- }
- });
+ @Test public void deleteMethodInvalidatesCache() throws Exception {
+ testMethodInvalidates("DELETE");
+ }
- URL url = server.getUrl("/");
- HttpURLConnection connection = openConnection(url);
- assertEquals(body, readAscii(connection));
- assertEquals(1, cacheCount.get());
+ 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();
+
+ URL url = server.getUrl("/");
+
+ assertEquals("A", readAscii(openConnection(url)));
+
+ HttpURLConnection invalidate = openConnection(url);
+ invalidate.setRequestMethod(requestMethod);
+ addRequestBodyIfNecessary(requestMethod, invalidate);
+ assertEquals("B", readAscii(invalidate));
+
+ 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 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<String> 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 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<String> 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 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<String> 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 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 {
+ // 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();
+
+ URL url = server.getUrl("/");
+
+ URLConnection range = openConnection(url);
+ range.addRequestProperty("Range", "bytes=1000-1001");
+ assertEquals("AA", readAscii(range));
+
+ 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<String> 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<String> 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();
- @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
- testResponseCaching(TransferKind.FIXED_LENGTH);
+ 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<String> actualCookies = new ArrayList<String>();
+ for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
+ actualCookies.add(cookie.toString());
}
+ assertEquals(Arrays.asList(expectedCookies), actualCookies);
+ }
- @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
- testResponseCaching(TransferKind.CHUNKED);
+ @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 responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
- testResponseCaching(TransferKind.END_OF_STREAM);
- }
+ private void assertNotCached(MockResponse response) throws Exception {
+ server.enqueue(response.setBody("A"));
+ server.enqueue(new MockResponse().setBody("B"));
+ server.play();
- /**
- * 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();
+ URL url = server.getUrl("/");
+ assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("B", readAscii(openConnection(url)));
+ }
- // 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());
+ /** @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<String> headers = new ArrayList<String>(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();
- assertEquals(1, cache.getWriteSuccessCount());
- assertEquals(0, cache.getWriteAbortCount());
-
- 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());
-
- assertEquals(-1, in.read());
- in.close();
- assertEquals(1, cache.getWriteSuccessCount());
- assertEquals(0, cache.getWriteAbortCount());
- assertEquals(2, cache.getRequestCount());
- assertEquals(1, cache.getHitCount());
+ break;
+ }
+ result.append((char) value);
}
+ return result.toString();
+ }
- @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();
+ private String readAscii(URLConnection connection) throws IOException {
+ return readAscii(connection, Integer.MAX_VALUE);
+ }
- HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
- connection.setSSLSocketFactory(sslContext.getSocketFactory());
- connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- assertEquals("ABC", readAscii(connection));
-
- // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
- String suite = connection.getCipherSuite();
- List<Certificate> localCerts = toListOrNull(connection.getLocalCertificates());
- List<Certificate> serverCerts = toListOrNull(connection.getServerCertificates());
- Principal peerPrincipal = connection.getPeerPrincipal();
- Principal localPrincipal = connection.getLocalPrincipal();
-
- connection = (HttpsURLConnection) client.open(server.getUrl("/")); // cached!
- connection.setSSLSocketFactory(sslContext.getSocketFactory());
- connection.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- assertEquals("ABC", readAscii(connection));
-
- assertEquals(2, cache.getRequestCount());
- assertEquals(1, cache.getNetworkCount());
- assertEquals(1, cache.getHitCount());
-
- assertEquals(suite, connection.getCipherSuite());
- assertEquals(localCerts, toListOrNull(connection.getLocalCertificates()));
- assertEquals(serverCerts, toListOrNull(connection.getServerCertificates()));
- assertEquals(peerPrincipal, connection.getPeerPrincipal());
- assertEquals(localPrincipal, connection.getLocalPrincipal());
+ private void reliableSkip(InputStream in, int length) throws IOException {
+ while (length > 0) {
+ length -= in.skip(length);
}
+ }
- @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();
-
- ResponseCache.setDefault(new InsecureResponseCache());
-
- HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
- connection1.setSSLSocketFactory(sslContext.getSocketFactory());
- connection1.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- assertEquals("ABC", readAscii(connection1));
-
- // Not cached!
- HttpsURLConnection connection2 = (HttpsURLConnection) client.open(server.getUrl("/"));
- connection2.setSSLSocketFactory(sslContext.getSocketFactory());
- connection2.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- assertEquals("DEF", readAscii(connection2));
+ private void assertGatewayTimeout(HttpURLConnection connection) throws IOException {
+ try {
+ connection.getInputStream();
+ fail();
+ } catch (FileNotFoundException expected) {
}
+ assertEquals(504, connection.getResponseCode());
+ assertEquals(-1, connection.getErrorStream().read());
+ }
- @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();
-
- HttpURLConnection connection = openConnection(server.getUrl("/"));
- assertEquals("ABC", readAscii(connection));
-
- connection = openConnection(server.getUrl("/")); // cached!
- assertEquals("ABC", readAscii(connection));
-
- assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects
- assertEquals(2, cache.getNetworkCount());
- assertEquals(2, cache.getHitCount());
- }
-
- @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();
-
- assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo"))));
- RecordedRequest request1 = server.takeRequest();
- assertEquals("GET /foo HTTP/1.1", request1.getRequestLine());
- assertEquals(0, request1.getSequenceNumber());
-
- assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar"))));
- RecordedRequest request2 = server.takeRequest();
- assertEquals("GET /bar HTTP/1.1", request2.getRequestLine());
- assertEquals(1, request2.getSequenceNumber());
-
- // 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 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<Map<String, List<String>>> requestHeadersRef
- = new AtomicReference<Map<String, List<String>>>();
- ResponseCache.setDefault(new ResponseCache() {
- @Override public CacheResponse get(URI uri, String requestMethod,
- Map<String, List<String>> 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 {
- /*
- * 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();
- }
-
- 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 clientPrematureDisconnectWithContentLengthHeader() throws IOException {
- testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
- }
-
- @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) {
- }
-
- 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 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();
-
- URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- URLConnection connection = openConnection(url);
- assertEquals("A", readAscii(connection));
- assertNull(connection.getHeaderField("Warning"));
- }
-
- @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<String> 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();
-
- 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 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("/?foo=bar");
- assertEquals("A", readAscii(openConnection(url)));
- assertEquals("B", readAscii(openConnection(url)));
- }
-
- @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<String> 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<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
- }
-
- @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"));
- }
-
- @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 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 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 requestMethodOptionsIsNotCached() throws Exception {
- testRequestMethod("OPTIONS", false);
- }
-
- @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 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 requestMethodDeleteIsNotCached() throws Exception {
- testRequestMethod("DELETE", false);
- }
-
- @Test public void requestMethodTraceIsNotCached() throws Exception {
- testRequestMethod("TRACE", false);
- }
-
- 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();
-
- URL url = server.getUrl("/");
-
- 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"));
- }
- }
-
- @Test public void postInvalidatesCache() throws Exception {
- testMethodInvalidates("POST");
- }
-
- @Test public void putInvalidatesCache() throws Exception {
- testMethodInvalidates("PUT");
- }
-
- @Test public void deleteMethodInvalidatesCache() throws Exception {
- testMethodInvalidates("DELETE");
- }
-
- 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();
-
- URL url = server.getUrl("/");
-
- assertEquals("A", readAscii(openConnection(url)));
-
- HttpURLConnection invalidate = openConnection(url);
- invalidate.setRequestMethod(requestMethod);
- addRequestBodyIfNecessary(requestMethod, invalidate);
- assertEquals("B", readAscii(invalidate));
-
- 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 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<String> 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 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<String> 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 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<String> 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 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 {
- /*
- * 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();
-
- URL url = server.getUrl("/");
-
- URLConnection range = openConnection(url);
- range.addRequestProperty("Range", "bytes=1000-1001");
- assertEquals("AA", readAscii(range));
-
- 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<String> 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<String> 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<String> actualCookies = new ArrayList<String>();
- 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) {
+ 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<String> headers = new ArrayList<String>(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<String> 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 <T> List<T> 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<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
- if (h.next().startsWith("Content-Length:")) {
- h.remove();
- break;
- }
- }
- }
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> 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<String, List<String>> 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);
- }
+ }
+ return response;
}
-
- private <T> List<T> 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<String, List<String>> 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<String, List<String>> getHeaders() throws IOException {
- return response.getHeaders();
- }
- };
- }
- 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 377227f..32649cf 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/RawHeadersTest.java
@@ -15,49 +15,50 @@
*/
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.internal.http.RawHeaders;
import java.util.Arrays;
import java.util.List;
-import static org.junit.Assert.assertEquals;
import org.junit.Test;
-public final class RawHeadersTest {
- @Test public void parseNameValueBlock() {
- List<String> nameValueBlock = Arrays.asList(
- "cache-control",
- "no-cache, no-store",
- "set-cookie",
- "Cookie1\u0000Cookie2",
- "status", "200 OK"
- );
- 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));
- }
+import static org.junit.Assert.assertEquals;
- @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");
- List<String> nameValueBlock = rawHeaders.toNameValueBlock();
- List<String> expected = Arrays.asList(
- "cache-control",
- "no-cache, no-store",
- "set-cookie",
- "Cookie1\u0000Cookie2",
- "status", "200 OK"
- );
- assertEquals(expected, nameValueBlock);
- }
+public final class RawHeadersTest {
+ @Test public void parseNameValueBlock() {
+ List<String> 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<String> nameValueBlock = rawHeaders.toNameValueBlock();
+ List<String> 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.<String>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 14642e9..4dc2e32 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.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 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<String> requestedHosts = new ArrayList<String>();
+ 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<URI> requestedUris = new ArrayList<URI>();
+ List<Proxy> proxies = new ArrayList<Proxy>();
+ List<String> failures = new ArrayList<String>();
- 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<Proxy> 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!
+ @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()));
}
-
- @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<String> requestedHosts = new ArrayList<String>();
- 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<URI> requestedUris = new ArrayList<URI>();
- List<Proxy> proxies = new ArrayList<Proxy>();
- List<String> failures = new ArrayList<String>();
-
- @Override public List<Proxy> 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()));
- }
- }
+ }
}
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 4d15189..2e7c598 100644
--- a/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -20,12 +20,9 @@
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.http.HttpResponseCache;
+import com.squareup.okhttp.internal.RecordingAuthenticator;
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -39,7 +36,6 @@
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
import java.net.InetAddress;
-import java.net.PasswordAuthentication;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.ProxySelector;
@@ -65,2321 +61,2282 @@
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
-import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSession;
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 {
- /** base64("username:password") */
- private static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";
+ private MockWebServer server = new MockWebServer();
+ private MockWebServer server2 = new MockWebServer();
- private MockWebServer server = new MockWebServer();
- private MockWebServer server2 = new MockWebServer();
+ private final OkHttpClient client = new OkHttpClient();
+ private HttpResponseCache cache;
+ private String hostName;
- 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);
- }
+ 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();
- }
+ @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();
- }
+ @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();
+ @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<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
- assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
- assertEquals(newSet("e", "f"), new HashSet<String>(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) {
- }
+ 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<String, List<String>> requestHeaders = urlConnection.getRequestProperties();
+ assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("D")));
+ assertEquals(newSet("e", "f"), new HashSet<String>(requestHeaders.get("d")));
+ try {
+ requestHeaders.put("G", Arrays.asList("h"));
+ fail("Modified an unmodifiable view.");
+ } catch (UnsupportedOperationException 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"));
+ try {
+ requestHeaders.get("D").add("i");
+ fail("Modified an unmodifiable view.");
+ } catch (UnsupportedOperationException expected) {
}
-
- @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<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
- assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
- assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A")));
- assertEquals(newSet("c", "e"), new HashSet<String>(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));
+ try {
+ urlConnection.setRequestProperty(null, "j");
+ fail();
+ } catch (NullPointerException expected) {
}
-
- @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) {
- }
+ 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"));
- @Test public void serverSendsInvalidCodeTooLarge() throws Exception {
- server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK"));
- server.play();
+ 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:.*");
- HttpURLConnection urlConnection = client.open(server.getUrl("/"));
- try {
- urlConnection.getResponseCode();
- fail();
- } catch (IOException expected) {
- }
+ try {
+ urlConnection.addRequestProperty("N", "o");
+ fail("Set header after connect");
+ } catch (IllegalStateException 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) {
- }
+ try {
+ urlConnection.setRequestProperty("P", "q");
+ fail("Set header after connect");
+ } catch (IllegalStateException 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) {
- }
+ try {
+ urlConnection.getRequestProperties();
+ fail();
+ } catch (IllegalStateException expected) {
}
+ }
- @Test public void connectRetriesUntilConnectedOrFailed() throws Exception {
- server.play();
- URL url = server.getUrl("/foo");
- server.shutdown();
+ @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"));
+ }
- HttpURLConnection connection = client.open(url);
- try {
- connection.connect();
- fail();
- } catch (IOException expected) {
- }
- }
+ @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();
- @Test public void requestBodySurvivesRetriesWithFixedLength() throws Exception {
- testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH);
- }
-
- @Test public void requestBodySurvivesRetriesWithChunkedStreaming() throws Exception {
- testRequestBodySurvivesRetries(TransferKind.CHUNKED);
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ assertEquals(200, urlConnection.getResponseCode());
+ assertEquals("Fantastic", urlConnection.getResponseMessage());
+ assertEquals("HTTP/1.0 200 Fantastic", urlConnection.getHeaderField(null));
+ Map<String, List<String>> responseHeaders = urlConnection.getHeaderFields();
+ assertEquals(Arrays.asList("HTTP/1.0 200 Fantastic"), responseHeaders.get(null));
+ assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("A")));
+ assertEquals(newSet("c", "e"), new HashSet<String>(responseHeaders.get("a")));
+ try {
+ responseHeaders.put("N", Arrays.asList("o"));
+ fail("Modified an unmodifiable view.");
+ } catch (UnsupportedOperationException expected) {
}
-
- @Test public void requestBodySurvivesRetriesWithBufferedBody() throws Exception {
- testRequestBodySurvivesRetries(TransferKind.END_OF_STREAM);
+ 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));
+ }
- private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception {
- server.enqueue(new MockResponse().setBody("abc"));
- server.play();
+ @Test public void serverSendsInvalidResponseHeaders() throws Exception {
+ server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK"));
+ 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());
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ try {
+ urlConnection.getResponseCode();
+ fail();
+ } catch (IOException expected) {
}
+ }
- @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 serverSendsInvalidCodeTooLarge() throws Exception {
+ server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK"));
+ server.play();
- @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));
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ try {
+ urlConnection.getResponseCode();
+ fail();
+ } catch (IOException expected) {
}
+ }
- // 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);
+ @Test public void serverSendsInvalidCodeNotANumber() throws Exception {
+ server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK"));
+ server.play();
- 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"));
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ try {
+ urlConnection.getResponseCode();
+ fail();
+ } catch (IOException expected) {
}
+ }
- @Test public void connectionsArePooled() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
+ @Test public void serverSendsUnnecessaryWhitespace() throws Exception {
+ server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK"));
+ server.play();
- 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());
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
+ try {
+ urlConnection.getResponseCode();
+ fail();
+ } catch (IOException expected) {
}
+ }
- @Test public void chunkedConnectionsArePooled() throws Exception {
- MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
-
- server.enqueue(response);
- server.enqueue(response);
- server.enqueue(response);
- server.play();
+ @Test public void connectRetriesUntilConnectedOrFailed() throws Exception {
+ server.play();
+ URL url = server.getUrl("/foo");
+ server.shutdown();
- 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());
+ HttpURLConnection connection = client.open(url);
+ try {
+ connection.connect();
+ fail();
+ } catch (IOException expected) {
}
+ }
- @Test public void serverClosesSocket() throws Exception {
- testServerClosesOutput(DISCONNECT_AT_END);
- }
+ @Test public void requestBodySurvivesRetriesWithFixedLength() throws Exception {
+ testRequestBodySurvivesRetries(TransferKind.FIXED_LENGTH);
+ }
- @Test public void serverShutdownInput() throws Exception {
- testServerClosesOutput(SHUTDOWN_INPUT_AT_END);
- }
+ @Test public void requestBodySurvivesRetriesWithChunkedStreaming() throws Exception {
+ testRequestBodySurvivesRetries(TransferKind.CHUNKED);
+ }
- @Test public void serverShutdownOutput() throws Exception {
- testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END);
- }
+ @Test public void requestBodySurvivesRetriesWithBufferedBody() throws Exception {
+ testRequestBodySurvivesRetries(TransferKind.END_OF_STREAM);
+ }
- 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();
+ private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ 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);
+ // 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();
- // 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());
- }
+ HttpURLConnection connection = client.open(server.getUrl("/def"));
+ connection.setDoOutput(true);
+ transferKind.setForRequest(connection, 4);
+ connection.getOutputStream().write("body".getBytes("UTF-8"));
+ assertContent("abc", connection);
- enum WriteKind { BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS }
+ assertEquals("body", server.takeRequest().getUtf8Body());
+ }
- @Test public void chunkedUpload_byteByByte() throws Exception {
- doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
- }
+ @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 chunkedUpload_smallBuffers() throws Exception {
- doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
- }
+ @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));
+ }
- @Test public void chunkedUpload_largeBuffers() throws Exception {
- doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
- }
+ // 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);
- @Test public void fixedLengthUpload_byteByByte() throws Exception {
- doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
- }
+ server.enqueue(response);
+ server.enqueue(response);
+ server.play();
- @Test public void fixedLengthUpload_smallBuffers() throws Exception {
- doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
- }
+ assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+ assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+ }
- @Test public void fixedLengthUpload_largeBuffers() throws Exception {
- doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
- }
+ // 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"));
+ }
- private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
- int n = 512*1024;
- server.setBodyLimit(0);
- server.enqueue(new MockResponse());
- server.play();
+ @Test public void connectionsArePooled() throws Exception {
+ MockResponse response = new MockResponse().setBody("ABCDEFGHIJKLMNOPQR");
- 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());
- }
- }
+ server.enqueue(response);
+ server.enqueue(response);
+ server.enqueue(response);
+ server.play();
- @Test public void getResponseCodeNoResponseBody() throws Exception {
- server.enqueue(new MockResponse()
- .addHeader("abc: def"));
- 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());
+ }
- 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();
+ @Test public void chunkedConnectionsArePooled() throws Exception {
+ MockResponse response = new MockResponse().setChunkedBody("ABCDEFGHIJKLMNOPQR", 5);
- client.setSSLSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- HttpURLConnection connection = client.open(server.getUrl("/foo"));
+ server.enqueue(response);
+ server.enqueue(response);
+ server.enqueue(response);
+ server.play();
- assertContent("this response comes via HTTPS", connection);
+ 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());
+ }
- RecordedRequest request = server.takeRequest();
- assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- }
+ @Test public void serverClosesSocket() throws Exception {
+ testServerClosesOutput(DISCONNECT_AT_END);
+ }
- @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();
+ @Test public void serverShutdownInput() throws Exception {
+ testServerClosesOutput(SHUTDOWN_INPUT_AT_END);
+ }
- // The pool will only reuse sockets if the SSL socket factories are the same.
- SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
- RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+ @Test public void serverShutdownOutput() throws Exception {
+ testServerClosesOutput(SHUTDOWN_OUTPUT_AT_END);
+ }
- client.setSSLSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(hostnameVerifier);
- HttpURLConnection connection = client.open(server.getUrl("/"));
- assertContent("this response comes via HTTPS", connection);
+ 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();
- connection = client.open(server.getUrl("/"));
- assertContent("another response via HTTPS", connection);
+ 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);
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals(1, server.takeRequest().getSequenceNumber());
+ // 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());
+ }
- @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();
+ enum WriteKind {BYTE_BY_BYTE, SMALL_BUFFERS, LARGE_BUFFERS}
- // 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);
+ @Test public void chunkedUpload_byteByByte() throws Exception {
+ doUpload(TransferKind.CHUNKED, WriteKind.BYTE_BY_BYTE);
+ }
- 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 chunkedUpload_smallBuffers() throws Exception {
+ doUpload(TransferKind.CHUNKED, WriteKind.SMALL_BUFFERS);
+ }
- @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();
+ @Test public void chunkedUpload_largeBuffers() throws Exception {
+ doUpload(TransferKind.CHUNKED, WriteKind.LARGE_BUFFERS);
+ }
- client.setSSLSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- HttpURLConnection connection = client.open(server.getUrl("/foo"));
+ @Test public void fixedLengthUpload_byteByByte() throws Exception {
+ doUpload(TransferKind.FIXED_LENGTH, WriteKind.BYTE_BY_BYTE);
+ }
- assertContent("this response comes via SSL", connection);
+ @Test public void fixedLengthUpload_smallBuffers() throws Exception {
+ doUpload(TransferKind.FIXED_LENGTH, WriteKind.SMALL_BUFFERS);
+ }
- RecordedRequest request = server.takeRequest();
- assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- }
+ @Test public void fixedLengthUpload_largeBuffers() throws Exception {
+ doUpload(TransferKind.FIXED_LENGTH, WriteKind.LARGE_BUFFERS);
+ }
- /**
- * 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();
+ private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
+ int n = 512 * 1024;
+ server.setBodyLimit(0);
+ server.enqueue(new MockResponse());
+ 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);
+ HttpURLConnection conn = client.open(server.getUrl("/"));
+ conn.setDoOutput(true);
+ conn.setRequestMethod("POST");
+ if (uploadKind == TransferKind.CHUNKED) {
+ conn.setChunkedStreamingMode(-1);
+ } else {
+ conn.setFixedLengthStreamingMode(n);
}
-
- @Test public void connectViaProxyUsingProxySystemProperty() throws Exception {
- testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+ 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));
+ }
}
-
- @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception {
- testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+ 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());
}
+ }
- 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);
+ @Test public void getResponseCodeNoResponseBody() throws Exception {
+ server.enqueue(new MockResponse().addHeader("abc: def"));
+ server.play();
- RecordedRequest request = server.takeRequest();
- assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(), "Host: android.com");
+ 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 contentDisagreesWithContentLengthHeader() throws IOException {
- server.enqueue(new MockResponse()
- .setBody("abc\r\nYOU SHOULD NOT SEE THIS")
- .clearHeaders()
- .addHeader("Content-Length: 3"));
- server.play();
+ @Test public void connectViaHttps() throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+ server.play();
- assertContent("abc", client.open(server.getUrl("/")));
- }
+ client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ HttpURLConnection connection = client.open(server.getUrl("/foo"));
- @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");
+ assertContent("this response comes via HTTPS", connection);
- server.enqueue(mockResponse);
- server.play();
+ RecordedRequest request = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+ }
- assertContent("abc", client.open(server.getUrl("/")));
- }
+ @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();
- @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
- testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
- }
+ // The pool will only reuse sockets if the SSL socket factories are the same.
+ SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
+ RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
- // https should not use http proxy
- testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
- }
+ client.setSSLSocketFactory(clientSocketFactory);
+ client.setHostnameVerifier(hostnameVerifier);
+ HttpURLConnection connection = client.open(server.getUrl("/"));
+ assertContent("this response comes via HTTPS", connection);
- private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
- server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
- server.play();
+ connection = client.open(server.getUrl("/"));
+ assertContent("another response via HTTPS", connection);
- URL url = server.getUrl("/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- HttpURLConnection connection = proxyConfig.connect(server, client, url);
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ }
- assertContent("this response comes via HTTPS", connection);
+ @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();
- RecordedRequest request = server.takeRequest();
- assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- }
+ // 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);
- @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
- testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
+ 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) {
}
+ }
- /**
- * 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();
+ @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();
- 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();
+ client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ HttpURLConnection connection = client.open(server.getUrl("/foo"));
- 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 SSL", connection);
- assertContent("this response comes via a secure proxy", connection);
+ RecordedRequest request = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+ }
- 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");
+ /**
+ * 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();
- 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);
+ HttpURLConnection connection = client.open(server.getUrl("/foo"));
+ try {
+ connection.getInputStream();
+ fail();
+ } catch (SSLHandshakeException expected) {
+ assertTrue(expected.getCause() instanceof CertificateException);
}
+ assertEquals(0, server.getRequestCount());
+ }
- /**
- * Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912
- */
- @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache()
- throws Exception {
- initResponseCache();
+ @Test public void connectViaProxyUsingProxyArg() throws Exception {
+ testConnectViaProxy(ProxyConfig.CREATE_ARG);
+ }
- 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");
+ @Test public void connectViaProxyUsingProxySystemProperty() throws Exception {
+ testConnectViaProxy(ProxyConfig.PROXY_SYSTEM_PROPERTY);
+ }
- // 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());
+ @Test public void connectViaProxyUsingHttpProxySystemProperty() throws Exception {
+ testConnectViaProxy(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+ }
- URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
- HttpURLConnection connection = client.open(url);
+ private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
+ MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
+ server.enqueue(mockResponse);
+ server.play();
- 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.
- }
+ URL url = new URL("http://android.com/foo");
+ HttpURLConnection connection = proxyConfig.connect(server, client, url);
+ assertContent("this response comes via a 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 request = server.takeRequest();
+ assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
+ assertContains(request.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 public void contentDisagreesWithContentLengthHeader() throws IOException {
+ server.enqueue(new MockResponse().setBody("abc\r\nYOU SHOULD NOT SEE THIS")
+ .clearHeaders()
+ .addHeader("Content-Length: 3"));
+ server.play();
- /**
- * Test which headers are sent unencrypted to the HTTP proxy.
- */
- @Test public void proxyConnectIncludesProxyHeadersOnly()
- throws IOException, InterruptedException {
- RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+ assertContent("abc", client.open(server.getUrl("/")));
+ }
- 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());
+ @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");
- 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);
+ server.enqueue(mockResponse);
+ server.play();
- 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");
+ assertContent("abc", client.open(server.getUrl("/")));
+ }
- RecordedRequest get = server.takeRequest();
- assertContains(get.getHeaders(), "Private: Secret");
- assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
- }
+ @Test public void connectViaHttpProxyToHttpsUsingProxyArgWithNoProxy() throws Exception {
+ testConnectViaDirectProxyToHttps(ProxyConfig.NO_PROXY);
+ }
- @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());
+ @Test public void connectViaHttpProxyToHttpsUsingHttpProxySystemProperty() throws Exception {
+ // https should not use http proxy
+ testConnectViaDirectProxyToHttps(ProxyConfig.HTTP_PROXY_SYSTEM_PROPERTY);
+ }
- URL url = new URL("https://android.com/foo");
- client.setSSLSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- HttpURLConnection connection = client.open(url);
- assertContent("A", connection);
+ private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
+ server.play();
- RecordedRequest connect1 = server.takeRequest();
- assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
- assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");
+ URL url = server.getUrl("/foo");
+ client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ HttpURLConnection connection = proxyConfig.connect(server, client, url);
- RecordedRequest connect2 = server.takeRequest();
- assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
- assertContains(connect2.getHeaders(), "Proxy-Authorization: Basic " + BASE_64_CREDENTIALS);
+ assertContent("this response comes via HTTPS", connection);
- RecordedRequest get = server.takeRequest();
- assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
- assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
- }
+ RecordedRequest request = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
+ }
- // 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());
+ @Test public void connectViaHttpProxyToHttpsUsingProxyArg() throws Exception {
+ testConnectViaHttpProxyToHttps(ProxyConfig.CREATE_ARG);
+ }
- 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");
+ /**
+ * 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);
+ }
- assertContent("this response comes via a proxy", connection);
- }
+ @Test public void connectViaHttpProxyToHttpsUsingHttpsProxySystemProperty() throws Exception {
+ testConnectViaHttpProxyToHttps(ProxyConfig.HTTPS_PROXY_SYSTEM_PROPERTY);
+ }
- @Test public void proxyWithConnectionReuse() throws IOException {
- SSLSocketFactory socketFactory = sslContext.getSocketFactory();
- RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+ /**
+ * 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(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());
+ 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(socketFactory);
- client.setHostnameVerifier(hostnameVerifier);
- assertContent("response 1", client.open(url));
- assertContent("response 2", client.open(url));
- }
+ URL url = new URL("https://android.com/foo");
+ client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(hostnameVerifier);
+ HttpURLConnection connection = proxyConfig.connect(server, client, url);
- @Test public void disconnectedConnection() throws IOException {
- server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
- server.play();
+ assertContent("this response comes via a secure proxy", connection);
- 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<String> calls = authCallsForHeader("WWW-Authenticate: Foo");
- assertEquals(Collections.<String>emptyList(), calls);
- }
-
- @Test public void nonStandardAuthenticationSchemeWithRealm() throws Exception {
- List<String> 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<String> 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<String> 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<String> 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<String> 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.<Integer>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 " + 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 " + 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 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();
-
- 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"));
- }
+ 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");
- @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();
+ 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);
+ }
- HttpURLConnection a = client.open(server.getUrl("/"));
- a.setRequestProperty("Connection", "close");
- assertEquals(200, a.getResponseCode());
+ /** Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912 */
+ @Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
+ initResponseCache();
- HttpURLConnection b = client.open(server.getUrl("/"));
- assertEquals(200, b.getResponseCode());
+ 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");
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals("When connection: close is used, each request should get its own connection",
- 0, server.takeRequest().getSequenceNumber());
+ // 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());
- @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());
+ URL url = new URL("https://android.com/foo");
+ client.setSSLSocketFactory(sslContext.getSocketFactory());
+ HttpURLConnection connection = client.open(url);
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals("When connection: close is used, each request should get its own connection",
- 0, server.takeRequest().getSequenceNumber());
+ 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.
}
- @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();
+ 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");
+ }
- URLConnection connection = client.open(server.getUrl("/"));
- assertEquals("This is the new location!",
- readAscii(connection.getInputStream(), Integer.MAX_VALUE));
+ 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);
+ }
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals("When connection: close is used, each request should get its own connection",
- 0, server.takeRequest().getSequenceNumber());
- }
+ /** Test which headers are sent unencrypted to the HTTP proxy. */
+ @Test public void proxyConnectIncludesProxyHeadersOnly()
+ throws IOException, InterruptedException {
+ RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- @Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
- server.enqueue(new MockResponse()
- .setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
- .setBody("This body is not allowed!"));
- server.play();
+ 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());
- URLConnection connection = client.open(server.getUrl("/"));
- assertEquals("This body is not allowed!",
- readAscii(connection.getInputStream(), Integer.MAX_VALUE));
- }
+ 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);
- @Test public void singleByteReadIsSigned() throws IOException {
- server.enqueue(new MockResponse().setBody(new byte[] { -2, -1 }));
- server.play();
+ 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");
- URLConnection connection = client.open(server.getUrl("/"));
- InputStream in = connection.getInputStream();
- assertEquals(254, in.read());
- assertEquals(255, in.read());
- assertEquals(-1, in.read());
- }
+ RecordedRequest get = server.takeRequest();
+ assertContains(get.getHeaders(), "Private: Secret");
+ assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+ }
- @Test public void flushAfterStreamTransmittedWithChunkedEncoding() throws IOException {
- testFlushAfterStreamTransmitted(TransferKind.CHUNKED);
- }
+ @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());
- @Test public void flushAfterStreamTransmittedWithFixedLength() throws IOException {
- testFlushAfterStreamTransmitted(TransferKind.FIXED_LENGTH);
- }
+ URL url = new URL("https://android.com/foo");
+ client.setSSLSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ HttpURLConnection connection = client.open(url);
+ assertContent("A", connection);
- @Test public void flushAfterStreamTransmittedWithNoLengthHeaders() throws IOException {
- testFlushAfterStreamTransmitted(TransferKind.END_OF_STREAM);
- }
+ RecordedRequest connect1 = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
+ assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");
- /**
- * 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();
+ RecordedRequest connect2 = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
+ assertContains(connect2.getHeaders(),
+ "Proxy-Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
- 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));
+ RecordedRequest get = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+ assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
+ }
- out.flush(); // dubious but permitted
- try {
- out.write("ghi".getBytes("UTF-8"));
- fail();
- } catch (IOException expected) {
- }
- }
+ // 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());
- @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();
+ 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");
- HttpURLConnection connection = client.open(server.getUrl("/"));
- try {
- connection.getInputStream();
- fail();
- } catch (IOException expected) {
- }
+ assertContent("this response comes via a proxy", connection);
+ }
- try {
- connection.getInputStream();
- fail();
- } catch (IOException expected) {
- }
- }
+ @Test public void proxyWithConnectionReuse() throws IOException {
+ SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+ RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- @Test public void dnsFailureThrowsIOException() throws IOException {
- HttpURLConnection connection = client.open(new URL("http://host.unlikelytld"));
- try {
- connection.connect();
- fail();
- } catch (IOException expected) {
- }
- }
+ 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());
- @Test public void malformedUrlThrowsUnknownHostException() throws IOException {
- HttpURLConnection connection = client.open(new URL("http:///foo.html"));
- try {
- connection.connect();
- fail();
- } catch (UnknownHostException expected) {
- }
- }
+ 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 getKeepAlive() throws Exception {
- MockWebServer server = new MockWebServer();
- server.enqueue(new MockResponse().setBody("ABC"));
- server.play();
+ @Test public void disconnectedConnection() throws IOException {
+ server.enqueue(new MockResponse().setBody("ABCDEFGHIJKLMNOPQR"));
+ 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) {
- }
+ 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) {
}
+ }
- /**
- * 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<String, List<String>> requestHeaders) throws IOException {
- return null;
- }
+ @Test public void disconnectBeforeConnect() throws IOException {
+ server.enqueue(new MockResponse().setBody("A"));
+ server.play();
- @Override
- public CacheRequest put(URI uri, URLConnection connection) throws IOException {
- return new CacheRequest() {
- @Override
- public void abort() {
- aborted.set(true);
- }
+ HttpURLConnection connection = client.open(server.getUrl("/"));
+ connection.disconnect();
- @Override
- public OutputStream getBody() throws IOException {
- return null;
- }
- };
- }
- });
+ assertContent("A", connection);
+ assertEquals(200, connection.getResponseCode());
+ }
- server.enqueue(new MockResponse().setBody("abcdef"));
- server.play();
+ @SuppressWarnings("deprecation") @Test public void defaultRequestProperty() throws Exception {
+ URLConnection.setDefaultRequestProperty("X-testSetDefaultRequestProperty", "A");
+ assertNull(URLConnection.getDefaultRequestProperty("X-setDefaultRequestProperty"));
+ }
- HttpURLConnection connection = client.open(server.getUrl("/"));
- InputStream in = connection.getInputStream();
- assertEquals("abc", readAscii(in, 3));
+ /**
+ * 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();
- assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
+ 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());
+ assertEquals(-1, connection.getContentLength());
+
+ RecordedRequest request = server.takeRequest();
+ assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+ }
+
+ @Test public void clientConfiguredGzipContentEncoding() throws Exception {
+ byte[] bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"));
+ server.enqueue(new MockResponse()
+ .setBody(bodyBytes)
+ .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));
+ assertEquals(bodyBytes.length, connection.getContentLength());
+
+ 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);
}
- /**
- * 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();
+ 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 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
+ 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) {
}
- @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));
+ // 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<String> calls = authCallsForHeader("WWW-Authenticate: Foo");
+ assertEquals(Collections.<String>emptyList(), calls);
+ }
+
+ @Test public void nonStandardAuthenticationSchemeWithRealm() throws Exception {
+ List<String> 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<String> 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<String> 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<String> 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<String> 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.<Integer>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();
+
+ 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 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();
+ 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) {
}
- @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) {
- }
+ try {
+ connection.getInputStream();
+ fail();
+ } catch (IOException 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 dnsFailureThrowsIOException() throws IOException {
+ HttpURLConnection connection = client.open(new URL("http://host.unlikelytld"));
+ try {
+ connection.connect();
+ fail();
+ } catch (IOException 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 malformedUrlThrowsUnknownHostException() throws IOException {
+ HttpURLConnection connection = client.open(new URL("http:///foo.html"));
+ try {
+ connection.connect();
+ fail();
+ } catch (UnknownHostException expected) {
}
+ }
- @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 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) {
}
+ }
- @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();
- }
+ /** 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<String, List<String>> requestHeaders) throws IOException {
+ return null;
+ }
- @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();
- }
+ @Override
+ public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ return new CacheRequest() {
+ @Override
+ public void abort() {
+ aborted.set(true);
+ }
- @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<String> headers, String header) {
- assertTrue(headers.toString(), headers.contains(header));
- }
-
- private void assertContainsNoneMatching(List<String> headers, String pattern) {
- for (String header : headers) {
- if (header.matches(pattern)) {
- fail("Header " + header + " matches " + pattern);
- }
- }
- }
-
- private Set<String> newSet(String... elements) {
- return new HashSet<String>(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<String> 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<String> headers, String header) {
+ assertTrue(headers.toString(), headers.contains(header));
+ }
+
+ private void assertContainsNoneMatching(List<String> headers, String pattern) {
+ for (String header : headers) {
+ if (header.matches(pattern)) {
+ fail("Header " + header + " matches " + pattern);
+ }
+ }
+ }
+
+ private Set<String> newSet(String... elements) {
+ return new HashSet<String>(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<String> 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<String> calls = new ArrayList<String>();
+
+ 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<String> calls = new ArrayList<String>();
-
- 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<String> result = new ArrayList<String>();
- 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 RecordingHostnameVerifier implements HostnameVerifier {
- private final List<String> calls = new ArrayList<String>();
+ private String certificatesToString(X509Certificate[] certificates) {
+ List<String> result = new ArrayList<String>();
+ for (X509Certificate certificate : certificates) {
+ result.add(certificate.getSubjectDN() + " " + certificate.getSerialNumber());
+ }
+ return result.toString();
+ }
+ }
- public boolean verify(String hostname, SSLSession session) {
- calls.add("verify " + hostname);
- return true;
- }
+ private static class FakeProxySelector extends ProxySelector {
+ List<Proxy> proxies = new ArrayList<Proxy>();
+
+ @Override public List<Proxy> 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);
}
- private static class RecordingAuthenticator extends Authenticator {
- private final List<String> calls = new ArrayList<String>();
- private final PasswordAuthentication authentication;
-
- public RecordingAuthenticator(PasswordAuthentication authentication) {
- this.authentication = authentication;
- }
-
- 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 public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
}
-
- private static class FakeProxySelector extends ProxySelector {
- List<Proxy> proxies = new ArrayList<Proxy>();
-
- @Override public List<Proxy> 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 0f8edca..6ca3756 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.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<URI> uriReference = new AtomicReference<URI>();
+
+ 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<String, List<String>> 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<URI> uriReference = new AtomicReference<URI>();
-
- 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<String, List<String>> 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
new file mode 100644
index 0000000..f7de138
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/mockspdyserver/MockSpdyServer.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal.mockspdyserver;
+
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.QueueDispatcher;
+import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
+import com.squareup.okhttp.internal.spdy.SpdyConnection;
+import com.squareup.okhttp.internal.spdy.SpdyStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+/** 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<Socket> openClientSockets =
+ Collections.newSetFromMap(new ConcurrentHashMap<Socket, Boolean>());
+ private int port = -1;
+ private final BlockingQueue<RecordedRequest> requestQueue =
+ new LinkedBlockingQueue<RecordedRequest>();
+
+ public MockSpdyServer(SSLSocketFactory sslSocketFactory) {
+ this.sslSocketFactory = sslSocketFactory;
+ }
+
+ public String getHostName() {
+ try {
+ return InetAddress.getLocalHost().getHostName();
+ } catch (UnknownHostException e) {
+ throw new AssertionError();
+ }
+ }
+
+ 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<Socket> 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 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<String> spdyHeaders = stream.getRequestHeaders();
+ List<String> httpHeaders = new ArrayList<String>();
+ String method = "<:method omitted>";
+ String path = "<:path omitted>";
+ String version = "<:version omitted>";
+ for (Iterator<String> 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<Integer> 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<String> spdyHeaders = new ArrayList<String>();
+ 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
new file mode 100644
index 0000000..b8afeb2
--- /dev/null
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/HttpOverSpdyTest.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.internal.RecordingAuthenticator;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpResponseCache;
+import com.squareup.okhttp.internal.mockspdyserver.MockSpdyServer;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Authenticator;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+import java.net.InetAddress;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.zip.GZIPOutputStream;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+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 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);
+ }
+
+ @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<String, List<String>> 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 <T> void assertContains(Collection<T> 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<String> 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();
+ }
+}
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 652786a..54e058b 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
@@ -18,6 +18,7 @@
import com.squareup.okhttp.internal.Util;
import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -31,190 +32,231 @@
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
-/**
- * Replays prerecorded outgoing frames and records incoming frames.
- */
-public final class MockSpdyPeer {
- private int frameCount = 0;
- private final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- private final SpdyWriter spdyWriter = new SpdyWriter(bytesOut);
- private final List<OutFrame> outFrames = new ArrayList<OutFrame>();
- private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
- private int port;
- private final Executor executor = Executors.newCachedThreadPool(
- Threads.newThreadFactory("MockSpdyPeer", true));
+/** 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<OutFrame> outFrames = new ArrayList<OutFrame>();
+ private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
+ 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<OutFrame> 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<String> nameValueBlock;
+ public byte[] data;
+ public Settings settings;
+
+ public InFrame(int sequence, SpdyReader reader) {
+ this.sequence = sequence;
+ this.reader = reader;
}
- public SpdyWriter sendFrame() {
- OutFrame frame = new OutFrame(frameCount++, bytesOut.size());
- outFrames.add(frame);
- 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;
}
- public int getPort() {
- return port;
+ @Override
+ public void synStream(int flags, int streamId, int associatedStreamId, int priority, int slot,
+ List<String> 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 InFrame takeFrame() throws InterruptedException {
- return inFrames.take();
+ @Override public void synReply(int flags, int streamId, List<String> nameValueBlock) {
+ if (this.type != -1) throw new IllegalStateException();
+ this.type = SpdyConnection.TYPE_SYN_REPLY;
+ this.streamId = streamId;
+ this.flags = flags;
+ this.nameValueBlock = nameValueBlock;
}
- public void play() throws IOException {
- final ServerSocket serverSocket = new ServerSocket(0);
- serverSocket.setReuseAddress(true);
- this.port = serverSocket.getLocalPort();
- executor.execute(new Runnable() {
- @Override public void run() {
- try {
- readAndWriteFrames(serverSocket);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- });
+ @Override public void headers(int flags, int streamId, List<String> nameValueBlock) {
+ if (this.type != -1) throw new IllegalStateException();
+ this.type = SpdyConnection.TYPE_HEADERS;
+ this.streamId = streamId;
+ this.flags = flags;
+ this.nameValueBlock = nameValueBlock;
}
- private void readAndWriteFrames(ServerSocket serverSocket) throws IOException {
- Socket socket = serverSocket.accept();
- OutputStream out = socket.getOutputStream();
- InputStream in = socket.getInputStream();
- SpdyReader reader = new SpdyReader(in);
-
- Iterator<OutFrame> 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 end;
- if (outFramesIterator.hasNext()) {
- nextOutFrame = outFramesIterator.next();
- end = nextOutFrame.start;
- } else {
- end = outBytes.length;
- }
-
- // write a frame
- out.write(outBytes, start, end - start);
-
- } else {
- // read a frame
- InFrame inFrame = new InFrame(i, reader);
- reader.nextFrame(inFrame);
- inFrames.add(inFrame);
- }
- }
+ @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);
}
- public Socket openSocket() throws IOException {
- return new Socket("localhost", port);
+ @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;
}
- private static class OutFrame {
- private final int sequence;
- private final int start;
-
- private OutFrame(int sequence, int start) {
- this.sequence = sequence;
- this.start = start;
- }
+ @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;
}
- 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 statusCode;
- public List<String> 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, List<String> 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.nameValueBlock = nameValueBlock;
- }
-
- @Override public void synReply(int flags, int streamId, List<String> 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<String> 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) {
- if (this.type != -1) throw new IllegalStateException();
- this.type = SpdyConnection.TYPE_GOAWAY;
- this.flags = flags;
- this.streamId = lastGoodStreamId;
- }
+ @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;
+ }
+ }
}
\ 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 1c44493..a906fc7 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
@@ -15,7 +15,8 @@
*/
package com.squareup.okhttp.internal.spdy;
-import com.squareup.okhttp.internal.spdy.Settings;
+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;
@@ -25,127 +26,130 @@
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));
- @Test public void isPersisted() {
- Settings settings = new Settings();
+ assertEquals(-3, settings.getClientCertificateVectorSize(-3));
+ settings.set(Settings.CLIENT_CERTIFICATE_VECTOR_SIZE, 0, 117);
+ assertEquals(117, settings.getClientCertificateVectorSize(-3));
+ }
- // Initially false.
- assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+ @Test public void isPersisted() {
+ Settings settings = new Settings();
- // Set no flags.
- settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
- assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+ // Initially false.
+ 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 no flags.
+ settings.set(Settings.ROUND_TRIP_TIME, 0, 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 wrong flag.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
+ assertFalse(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 the right flag.
+ settings.set(Settings.ROUND_TRIP_TIME, 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));
+ // Set multiple flags.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0);
+ assertTrue(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 the flag.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 0);
+ assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
- @Test public void persistValue() {
- Settings settings = new Settings();
+ // Clear all flags.
+ settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
+ assertFalse(settings.isPersisted(Settings.ROUND_TRIP_TIME));
+ }
- // Initially false.
- assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+ @Test public void persistValue() {
+ Settings settings = new Settings();
- // Set no flags.
- settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
- assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+ // Initially false.
+ 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 no flags.
+ settings.set(Settings.ROUND_TRIP_TIME, 0, 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 wrong flag.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 0);
+ assertFalse(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 the right flag.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE, 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));
+ // Set multiple flags.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSIST_VALUE | PERSISTED, 0);
+ assertTrue(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 the flag.
+ settings.set(Settings.ROUND_TRIP_TIME, PERSISTED, 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);
+ // Clear all flags.
+ settings.set(Settings.ROUND_TRIP_TIME, 0, 0);
+ assertFalse(settings.persistValue(Settings.ROUND_TRIP_TIME));
+ }
- 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);
+ @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);
- 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));
- }
+ 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));
+ }
}
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 d51fed7..7dd23f6 100644
--- a/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
+++ b/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
@@ -16,10 +16,24 @@
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;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.FLAG_UNIDIRECTIONAL;
+import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_INTERNAL_ERROR;
+import static com.squareup.okhttp.internal.spdy.SpdyConnection.GOAWAY_PROTOCOL_ERROR;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_DATA;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_GOAWAY;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_NOOP;
@@ -27,893 +41,992 @@
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_RST_STREAM;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_SYN_REPLY;
import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_SYN_STREAM;
+import static com.squareup.okhttp.internal.spdy.SpdyConnection.TYPE_WINDOW_UPDATE;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_FLOW_CONTROL_ERROR;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_INVALID_STREAM;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_PROTOCOL_ERROR;
import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_REFUSED_STREAM;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.Arrays;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
+import static com.squareup.okhttp.internal.spdy.SpdyStream.RST_STREAM_IN_USE;
+import static com.squareup.okhttp.internal.spdy.SpdyStream.WINDOW_UPDATE_THRESHOLD;
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();
- @Test public void clientCreatesStreamAndServerReplies() throws Exception {
- // write the mocking script
- peer.acceptFrame();
- peer.sendFrame().synReply(0, 1, Arrays.asList("a", "android"));
- peer.sendFrame().data(SpdyConnection.FLAG_FIN, 1, "robot".getBytes("UTF-8"));
- peer.acceptFrame();
- peer.play();
+ // 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());
+ }
- // 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 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);
- 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 {
+ 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 headersOnlyStreamIsClosedImmediately() 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 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);
+ }
- SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
- connection.newStream(Arrays.asList("a", "android"), false, false);
- 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();
+
+ // 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 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 goaway = peer.takeFrame();
+ assertEquals(TYPE_GOAWAY, goaway.type);
+ assertEquals(GOAWAY_INTERNAL_ERROR, goaway.statusCode);
+ }
- // 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 close() throws Exception {
+ // write the mocking script
+ peer.acceptFrame(); // SYN_STREAM
+ peer.acceptFrame(); // GOAWAY
+ peer.acceptFrame(); // RST_STREAM
+ 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();
+ 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 serverCreatesStreamAndClientReplies() throws Exception {
- // write the mocking script
- peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
- peer.acceptFrame();
- 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
- 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("b", "banana"), true);
+ @Test public void closeCancelsPings() throws Exception {
+ // write the mocking script
+ peer.acceptFrame(); // PING
+ peer.acceptFrame(); // GOAWAY
+ peer.play();
- }
- };
- new SpdyConnection.Builder(true, peer.openSocket())
- .handler(handler)
- .build();
+ // play it back
+ SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket()).build();
+ Ping ping = connection.ping();
+ connection.close();
+ assertEquals(-1, ping.roundTripTime());
+ }
- // 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 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 replyWithNoData() throws Exception {
- // write the mocking script
- peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
- peer.acceptFrame();
- 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 {
- stream.reply(Arrays.asList("b", "banana"), false);
- receiveCount.incrementAndGet();
- }
- };
- new SpdyConnection.Builder(true, peer.openSocket())
- .handler(handler)
- .build();
+ @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();
- // 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();
+ 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) {
}
- @Test public void noop() throws Exception {
- // write the mocking script
- peer.acceptFrame();
- 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
- SpdyConnection connection = new SpdyConnection.Builder(true, peer.openSocket())
- .handler(REJECT_INCOMING_STREAMS)
- .build();
- connection.noop();
+ @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 ping = peer.takeFrame();
- assertEquals(TYPE_NOOP, ping.type);
- assertEquals(0, ping.flags);
+ // 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 serverPingsClient() throws Exception {
- // write the mocking script
- peer.sendFrame().ping(0, 2);
- peer.acceptFrame();
- 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
- 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);
+ 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 clientPingsServer() throws Exception {
- // write the mocking script
- peer.acceptFrame();
- 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();
- peer.sendFrame().ping(0, 3); // This ping will not be returned.
- peer.sendFrame().ping(0, 4);
- peer.acceptFrame();
- 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();
- 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.acceptFrame(); // TYPE_DATA
- peer.acceptFrame(); // TYPE_DATA with FLAG_FIN
- peer.sendFrame().ping(0, 2);
- peer.acceptFrame(); // PING response
- 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();
}
- 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(2, ping.streamId);
- }
-
- @Test public void serverClosesClientOutputStream() throws Exception {
- // write the mocking script
- peer.acceptFrame(); // SYN_STREAM
- peer.sendFrame().synReset(1, SpdyStream.RST_CANCEL);
- 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, 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);
- }
-
- /**
- * 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().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();
- 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 e) {
- assertEquals("stream was reset: PROTOCOL_ERROR", e.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_PROTOCOL_ERROR, rstStream.statusCode);
- }
-
- @Test public void remoteDoubleSynStream() throws Exception {
- // write the mocking script
- peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("a", "android"));
- peer.acceptFrame();
- peer.sendFrame().synStream(0, 2, 0, 0, Arrays.asList("b", "banana"));
- peer.acceptFrame();
- 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();
- 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();
- 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();
- peer.sendFrame().synReply(0, 1, Arrays.asList("b", "banana"));
- peer.sendFrame().data(0, 1, new byte[64 * 1024 + 1]);
- peer.acceptFrame();
- peer.sendFrame().ping(0, 2); // Ping just to make sure the stream was fastforwarded.
- peer.acceptFrame();
- 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();
- peer.sendFrame().synReset(1, RST_REFUSED_STREAM);
- peer.sendFrame().ping(0, 2);
- peer.acceptFrame();
- 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);
- 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, Arrays.asList("b", "banana")); // 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();
- ping.roundTripTime(); // Ensure that the SYN STREAM has been received.
- assertEquals(1, connection.openStreamCount());
-
- // 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);
- }
-
- @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();
- 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);
- }
-
- @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.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());
-
- // 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 e) {
- assertEquals("stream was reset: PROTOCOL_ERROR", e.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);
- }
-
- 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);
- }
+ }
+ }.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 1b5574d..7371f2e 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.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<String> 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<String> 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<String> 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<String> 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("<a href='" + file + "'>" + file + "</a><br>");
+ }
+ 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 <base directory>");
+ 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<String> protocols() {
- return Arrays.asList("spdy/2");
- }
- @Override public void protocolSelected(String protocol) {
- System.out.println("PROTOCOL SELECTED: " + protocol);
- }
- });
- return sslSocket;
- }
-
- @Override public void receive(final SpdyStream stream) throws IOException {
- List<String> requestHeaders = stream.getRequestHeaders();
- String path = null;
- for (int i = 0; i < requestHeaders.size(); i += 2) {
- String s = requestHeaders.get(i);
- if ("url".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<String> 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<String> 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("<a href='" + file + "'>" + file + "</a><br>");
- }
- 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 <base directory>");
- 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();
+ }
}