Fix regression in the HTTP request line am: af79cbf93e
am: ffe9365e03
Change-Id: Ie8eb743f9ca9afefc508992b9418e317a21ca6ba
diff --git a/.buildscript/deploy_snapshot.sh b/.buildscript/deploy_snapshot.sh
new file mode 100755
index 0000000..4e141ca
--- /dev/null
+++ b/.buildscript/deploy_snapshot.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
+#
+# Adapted from https://coderwall.com/p/9b_lfq and
+# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
+
+SLUG="square/okhttp"
+JDK="oraclejdk8"
+BRANCH="master"
+
+set -e
+
+if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
+ echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
+elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then
+ echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'."
+elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+ echo "Skipping snapshot deployment: was pull request."
+elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
+ echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
+else
+ echo "Deploying snapshot..."
+ mvn clean source:jar javadoc:jar deploy --settings=".buildscript/settings.xml" -Dmaven.test.skip=true
+ echo "Snapshot deployed!"
+fi
diff --git a/.buildscript/settings.xml b/.buildscript/settings.xml
new file mode 100644
index 0000000..91f444b
--- /dev/null
+++ b/.buildscript/settings.xml
@@ -0,0 +1,9 @@
+<settings>
+ <servers>
+ <server>
+ <id>sonatype-nexus-snapshots</id>
+ <username>${env.CI_DEPLOY_USERNAME}</username>
+ <password>${env.CI_DEPLOY_PASSWORD}</password>
+ </server>
+ </servers>
+</settings>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..226a3f3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+.classpath
+.project
+.settings
+eclipsebin
+
+bin
+gen
+build
+out
+lib
+
+target
+pom.xml.*
+release.properties
+
+.idea
+*.iml
+*.ipr
+*.iws
+classes
+
+obj
+
+.DS_Store
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..d29f0b1
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "okhttp-hpacktests/src/test/resources/hpack-test-case"]
+ path = okhttp-hpacktests/src/test/resources/hpack-test-case
+ url = git://github.com/http2jp/hpack-test-case.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ed135a7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,26 @@
+language: java
+
+jdk:
+ - oraclejdk7
+ - oraclejdk8
+
+after_success:
+ - .buildscript/deploy_snapshot.sh
+
+env:
+ global:
+ - secure: "S0BTJVrF4fUCwhTdmoQY6LYr5r1wgXZ/p8lc5bIgUUsc1Ckalwt7s/GDwPuLJ4702sI5t56Eye2iEIMUjeFJKqebZRsX1C5oYsYFxGi3BGlepstYpmj0gLXuSWqCLniS9zmHXCxLhLkC6KxPVjhDlbq76XQx0o3K1J8oEIj/PCE="
+ - secure: "awV7yLXURjlPbTOladsNDZk74KYCNXoiZpAP0gQFfK4Sc0fc7+kg8z/yhdWXeTxjsIZ6m0dVDHTqnH8ytnydwXpBam8JdQJ+EAWA6R3Svq1BR1bzl/PcZUoz+Xn8lMXdU3yA1p4qtQlUhMxwsE3MOVe24HSDJPAu4XeWFj1j3qo="
+
+branches:
+ except:
+ - gh-pages
+
+notifications:
+ email: false
+
+sudo: false
+
+cache:
+ directories:
+ - $HOME/.m2
diff --git a/Android.mk b/Android.mk
index d98f068..f82a7b5 100644
--- a/Android.mk
+++ b/Android.mk
@@ -19,6 +19,7 @@
okhttp_common_src_files += $(call all-java-files-under,okhttp-urlconnection/src/main/java)
okhttp_common_src_files += $(call all-java-files-under,okhttp-android-support/src/main/java)
okhttp_common_src_files += $(call all-java-files-under,okio/okio/src/main/java)
+
okhttp_system_src_files := $(filter-out %/Platform.java, $(okhttp_common_src_files))
okhttp_system_src_files += $(call all-java-files-under, android/main/java)
@@ -33,12 +34,9 @@
okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/main/java)
okhttp_test_src_files += $(call all-java-files-under,mockwebserver/src/test/java)
-# Exclude tests Android currently has problems with:
-# 1) Parameterized (requires JUnit 4.11).
-# 2) New dependencies like gson.
+# Exclude test Android currently has problems with due to @Parameterized (requires JUnit 4.11).
okhttp_test_src_excludes := \
- okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java \
- okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java
+ okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java
okhttp_test_src_files := \
$(filter-out $(okhttp_test_src_excludes), $(okhttp_test_src_files))
@@ -48,7 +46,7 @@
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(okhttp_system_src_files)
LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt
+LOCAL_JAVA_LIBRARIES := core-oj core-libart
LOCAL_NO_STANDARD_LIBRARIES := true
LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
LOCAL_JAVA_LANGUAGE_VERSION := 1.7
@@ -59,7 +57,7 @@
LOCAL_MODULE := okhttp-nojarjar
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt
+LOCAL_JAVA_LIBRARIES := core-oj core-libart
LOCAL_NO_STANDARD_LIBRARIES := true
LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
LOCAL_JAVA_LANGUAGE_VERSION := 1.7
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00ad4c3..6ceddcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,136 @@
Change Log
==========
+## Version 2.7.5
+
+_2016-02-25_
+
+ * Fix: Change the certificate pinner to always build full chains. This
+ prevents a potential crash when using certificate pinning with the Google
+ Play Services security provider.
+
+
+## Version 2.7.4
+
+_2016-02-07_
+
+ * Fix: Don't crash when finding the trust manager if the Play Services (GMS)
+ security provider is installed.
+ * Fix: The previous release introduced a performance regression on Android,
+ caused by looking up CA certificates. This is now fixed.
+
+
+## Version 2.7.3
+
+_2016-02-06_
+
+ * Fix: Permit the trusted CA root to be pinned by `CertificatePinner`.
+
+
+## Version 2.7.2
+
+_2016-01-07_
+
+ * Fix: Don't eagerly release stream allocations on cache hits. We might still
+ need them to handle redirects.
+
+
+## Version 2.7.1
+
+_2016-01-01_
+
+ * Fix: Don't do a health check on newly-created connections. This is
+ unnecessary work that could put the client in an inconsistent state if the
+ health check fails.
+
+
+## Version 2.7.0
+
+_2015-12-12_
+
+ * **Rewritten connection management.** Previously OkHttp's connection pool
+ managed both idle and active connections for HTTP/2, but only idle
+ connections for HTTP/1.x. Wth this update the connection pool manages both
+ idle and active connections for everything. OkHttp now detects and warns on
+ connections that were allocated but never released, and will enforce HTTP/2
+ stream limits. This update also fixes `Call.cancel()` to not do I/O on the
+ calling thread.
+ * Fix: Don't log gzipped data in the logging interceptor.
+ * Fix: Don't resolve DNS addresses when connecting through a SOCKS proxy.
+ * Fix: Drop the synthetic `OkHttp-Selected-Protocol` response header.
+ * Fix: Support 204 and 205 'No Content' replies in the logging interceptor.
+ * New: Add `Call.isExecuted()`.
+
+
+## Version 2.6.0
+
+_2015-11-22_
+
+ * **New Logging Interceptor.** The `logging-interceptor` subproject offers
+ simple request and response logging. It may be configured to log headers and
+ bodies for debugging. It requires this Maven dependency:
+
+ ```xml
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>logging-interceptor</artifactId>
+ <version>2.6.0</version>
+ </dependency>
+ ```
+
+ Configure basic logging like this:
+
+ ```java
+ HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
+ loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
+ client.networkInterceptors().add(loggingInterceptor);
+ ```
+
+ **Warning:** Avoid `Level.HEADERS` and `Level.BODY` in production because
+ they could leak passwords and other authentication credentials to insecure
+ logs.
+
+ * **WebSocket API now uses `RequestBody` and `ResponseBody` for messages.**
+ This is a backwards-incompatible API change.
+
+ * **The DNS service is now pluggable.** In some situations this may be useful
+ to manually prioritize specific IP addresses.
+
+ * Fix: Don't throw when converting an `HttpUrl` to a `java.net.URI`.
+ Previously URLs with special characters like `|` and `[` would break when
+ subjected to URI’s overly-strict validation.
+ * Fix: Don't re-encode `+` as `%20` in encoded URL query strings. OkHttp
+ prefers `%20` when doing its own encoding, but will retain `+` when that is
+ provided.
+ * Fix: Enforce that callers call `WebSocket.close()` on IO errors. Error
+ handling in WebSockets is significantly improved.
+ * Fix: Don't use SPDY/3 style header concatenation for HTTP/2 request headers.
+ This could have corrupted requests where multiple headers had the same name,
+ as in cookies.
+ * Fix: Reject bad characters in the URL hostname. Previously characters like
+ `\0` would cause a late crash when building the request.
+ * Fix: Allow interceptors to change the request method.
+ * Fix: Don’t use the request's `User-Agent` or `Proxy-Authorization` when
+ connecting to an HTTPS server via an HTTP tunnel. The `Proxy-Authorization`
+ header was being leaked to the origin server.
+ * Fix: Digits may be used in a URL scheme.
+ * Fix: Improve connection timeout recovery.
+ * Fix: Recover from `getsockname` crashes impacting Android releases prior to
+ 4.2.2.
+ * Fix: Drop partial support for HTTP/1.0. Previously OkHttp would send
+ `HTTP/1.0` on connections after seeing a response with `HTTP/1.0`. The fixed
+ behavior is consistent with Firefox and Chrome.
+ * Fix: Allow a body in `OPTIONS` requests.
+ * Fix: Don't percent-encode non-ASCII characters in URL fragments.
+ * Fix: Handle null fragments.
+ * Fix: Don’t crash on interceptors that throw `IOException` before a
+ connection is attempted.
+ * New: Support [WebDAV][webdav] HTTP methods.
+ * New: Buffer WebSocket frames for better performance.
+ * New: Drop support for `TLS_DHE_DSS_WITH_AES_128_CBC_SHA`, our only remaining
+ DSS cipher suite. This is consistent with Firefox and Chrome which have also
+ dropped these cipher suite.
+
## Version 2.5.0
_2015-08-25_
@@ -22,7 +152,7 @@
where changing a URL from `http` to `https` would leave it on port 80.
* **Okio has been updated to 1.6.0.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -81,7 +211,7 @@
Both are permitted-by-spec, but `%20` requires fewer special cases.
* **Okio has been updated to 1.4.0.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -93,7 +223,7 @@
Passing null will now fail for request methods that require a body. Instead
use an empty body such as this one:
- ```
+ ```java
RequestBody.create(null, new byte[0]);
```
@@ -102,7 +232,7 @@
your app. You'll need to pin both the top-level domain and the `*.` domain
for full coverage.
- ```
+ ```java
client.setCertificatePinner(new CertificatePinner.Builder()
.add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
.add("*.publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
@@ -159,7 +289,7 @@
* **Okio updated to 1.3.0.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -264,14 +394,14 @@
To disable TLS fallback:
- ```
+ ```java
client.setConnectionSpecs(Arrays.asList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
```
To disable cleartext connections, permitting `https` URLs only:
- ```
+ ```java
client.setConnectionSpecs(Arrays.asList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS));
```
@@ -305,7 +435,7 @@
* **Okio updated to 1.0.1.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -385,7 +515,7 @@
agent.
* New: Guava-like API to create headers:
- ```
+ ```java
Headers headers = Headers.of(name1, value1, name2, value2, ...).
```
@@ -428,7 +558,7 @@
add the `okhttp-urlconnection` module to your project and use the
`OkUrlFactory` to create new instances of `HttpURLConnection`:
- ```
+ ```java
// OkHttp 1.x:
HttpURLConnection connection = client.open(url);
@@ -683,4 +813,5 @@
Initial release.
- [brick]: (https://noncombatant.org/2015/05/01/about-http-public-key-pinning/)
+ [brick]: https://noncombatant.org/2015/05/01/about-http-public-key-pinning/
+ [webdav]: https://tools.ietf.org/html/rfc4918
diff --git a/README.android b/README.android
index 0a1b91b..3ad84fc 100644
--- a/README.android
+++ b/README.android
@@ -7,16 +7,19 @@
Addition of classes in android/ :
- com.squareup.okhttp.internal.Platform - to replace the Platform class that
- comes with okhttp. No use of reflection.
+ comes with okhttp. Avoids use of reflection where possible.
- com.squareup.okhttp.Http(s)Handler - integration with Android's corelibs.
- com.squareup.okhttp.ConfigAwareConnectionPool - support for a
ConnectionPool that listens for network configuration changes.
- - com.squareup.okhttp.internal.Version - a hard-crafted version of
+ - com.squareup.okhttp.internal.Version - a hand-crafted version of
okhttp/src/main/java-templates/com/squareup/okhttp/internal/Version.java
for Android.
All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
- Commenting of code that references APIs not present on Android.
+ - @Ignore or comment out tests for functionality not supported on Android.
+ - Work around JUnit 4.11 not being available on Android by adding a
+ assertNotEquals() method to affect tests.
okio/ contains a snapshot of the Okio project. See okio/README.android for
details.
diff --git a/README.md b/README.md
index 4fde155..f10aaeb 100644
--- a/README.md
+++ b/README.md
@@ -11,12 +11,12 @@
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
- <version>2.5.0</version>
+ <version>2.6.0</version>
</dependency>
```
or Gradle:
```groovy
-compile 'com.squareup.okhttp:okhttp:2.5.0'
+compile 'com.squareup.okhttp:okhttp:2.6.0'
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
@@ -36,13 +36,13 @@
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>mockwebserver</artifactId>
- <version>2.5.0</version>
+ <version>2.6.0</version>
<scope>test</scope>
</dependency>
```
or Gradle:
```groovy
-testCompile 'com.squareup.okhttp:mockwebserver:2.5.0'
+testCompile 'com.squareup.okhttp:mockwebserver:2.6.0'
```
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
index 6b127b2..ca5048b 100644
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpsHandler.java
@@ -98,6 +98,12 @@
// Use Android's preferred fallback approach and cipher suite selection.
okHttpClient.setConnectionSpecs(SECURE_CONNECTION_SPECS);
+ // Android support certificate pinning via NetworkSecurityConfig so there is no need to
+ // also expose OkHttp's mechanism. The OkHttpClient underlying https HttpsURLConnections
+ // in Android should therefore always use the default certificate pinner, whose set of
+ // {@code hostNamesToPin} is empty.
+ okHttpClient.setCertificatePinner(CertificatePinner.DEFAULT);
+
// OkHttp does not automatically honor the system-wide HostnameVerifier set with
// HttpsURLConnection.setDefaultHostnameVerifier().
okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 322c875..f4828f2 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -16,30 +16,62 @@
*/
package com.squareup.okhttp.internal;
-import dalvik.system.SocketTagger;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.tls.RealTrustRootIndex;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
+
import java.io.IOException;
-import java.io.OutputStream;
+import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
-import com.squareup.okhttp.Protocol;
-
+import dalvik.system.SocketTagger;
import okio.Buffer;
/**
- * Access to proprietary Android APIs. Doesn't use reflection.
+ * Access to proprietary Android APIs. Avoids use of reflection where possible.
*/
-public final class Platform {
- private static final Platform PLATFORM = new Platform();
+// only tests should extend this class
+public class Platform {
+ private static final AtomicReference<Platform> INSTANCE_HOLDER
+ = new AtomicReference<>(new Platform());
+
+ // only for private use and in tests
+ protected Platform() {
+ }
public static Platform get() {
- return PLATFORM;
+ return INSTANCE_HOLDER.get();
+ }
+
+ /**
+ * Atomically replaces the Platform instance returned by future
+ * invocations of {@link #get()}, for use in tests.
+ * Invocations of this method should typically be followed by
+ * a try/finally block to reset the previous value:
+ *
+ * <pre>
+ * Platform p = getAndSetForTest(...);
+ * try {
+ * ...
+ * } finally {
+ * getAndSetForTest(p);
+ * }
+ * </pre>
+ *
+ * @return the previous value of {@link #get()}.
+ */
+ public static Platform getAndSetForTest(Platform platform) {
+ if (platform == null) {
+ throw new NullPointerException();
+ }
+ return INSTANCE_HOLDER.getAndSet(platform);
}
/** setUseSessionTickets(boolean) */
@@ -119,6 +151,56 @@
}
/**
+ * Stripped down/adapted from OkHttp's {@code Platform.Android.trustManager()}.
+ * OkHttp 2.7.5 uses this only for certificate pinning logic that we don't use
+ * on Android, so this method should never be called outside of OkHttp's tests.
+ * This method has been stripped down to the minimum for OkHttp's tests to pass.
+ */
+ public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ Class sslParametersClass;
+ try {
+ sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters");
+ return readFieldOrNull(context, X509TrustManager.class, "x509TrustManager");
+ }
+
+ /**
+ * Stripped down from OkHttp's implementation to the minimum to get OkHttp's tests
+ * to pass. OkHttp 2.7.5 uses this for certificate pinning logic which is unused
+ * in Android. This method should never be called outside of OkHttp's tests.
+ */
+ public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+ return new RealTrustRootIndex(trustManager.getAcceptedIssuers());
+ }
+
+ // Helper method from OkHttp's Platform.java
+ private static <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) {
+ for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) {
+ try {
+ Field field = c.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object value = field.get(instance);
+ if (value == null || !fieldType.isInstance(value)) return null;
+ return fieldType.cast(value);
+ } catch (NoSuchFieldException ignored) {
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+
+ // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate.
+ if (!fieldName.equals("delegate")) {
+ Object delegate = readFieldOrNull(instance, Object.class, "delegate");
+ if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName);
+ }
+
+ return null;
+ }
+
+ /**
* Returns the concatenation of 8-bit, length prefixed protocol names.
* http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
*/
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
index 2e5dcdd..9735913 100644
--- a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -24,19 +24,25 @@
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.List;
-
import javax.net.ssl.HandshakeCompletedListener;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
-
-import okio.ByteString;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
/**
* Tests for {@link Platform}.
@@ -77,6 +83,44 @@
assertEquals(selectedProtocol, platform.getSelectedProtocol(openSslSocket));
}
+ @Test public void rootTrustIndex_notNull_viaSocketFactory() throws Exception {
+ Platform platform = new Platform();
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(null, new TrustManager[] { TRUST_NO_ONE_TRUST_MANAGER }, new SecureRandom());
+ SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+ X509TrustManager trustManager = platform.trustManager(socketFactory);
+ assertNotNull(platform.trustRootIndex(trustManager));
+ }
+
+ @Test public void rootTrustIndex_notNull() throws Exception {
+ Platform platform = new Platform();
+ assertNotNull(platform.trustRootIndex(TRUST_NO_ONE_TRUST_MANAGER));
+ }
+
+ @Test public void trustManager() throws Exception {
+ Platform platform = new Platform();
+ SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
+ sslContext.init(null, new TrustManager[] { TRUST_NO_ONE_TRUST_MANAGER }, new SecureRandom());
+ SSLSocketFactory socketFactory = sslContext.getSocketFactory();
+ X509TrustManager trustManager = platform.trustManager(socketFactory);
+ assertEquals(TRUST_NO_ONE_TRUST_MANAGER, trustManager);
+ }
+
+ private static final X509TrustManager TRUST_NO_ONE_TRUST_MANAGER = new X509TrustManager() {
+ @Override public void checkClientTrusted(X509Certificate[] chain, String authType)
+ throws CertificateException {
+ throw new CertificateException();
+ }
+
+ @Override public void checkServerTrusted(X509Certificate[] chain, String authType) {
+ throw new AssertionError();
+ }
+
+ @Override public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ };
+
private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
private boolean useSessionTickets;
private String hostname;
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index d0d2566..b938848 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>benchmarks</artifactId>
diff --git a/deploy_website.sh b/deploy_website.sh
index bbeedc2..c9b7f15 100755
--- a/deploy_website.sh
+++ b/deploy_website.sh
@@ -3,9 +3,6 @@
set -ex
REPO="git@github.com:square/okhttp.git"
-GROUP_ID="com.squareup.okhttp"
-ARTIFACT_ID="okhttp"
-
DIR=temp-clone
# Delete any existing temporary website clone
@@ -20,28 +17,12 @@
# Checkout and track the gh-pages branch
git checkout -t origin/gh-pages
-# Delete everything
-rm -rf *
+# Delete everything that isn't versioned (1.x, 2.x)
+ls | grep -E -v '^\d+\.x$' | xargs rm -rf
# Copy website files from real repo
cp -R ../website/* .
-# Download the latest javadoc to directories like 'javadoc' or 'javadoc-urlconnection'.
-for DOCUMENTED_ARTIFACT in okhttp okhttp-urlconnection okhttp-apache
-do
- curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$DOCUMENTED_ARTIFACT&v=LATEST&c=javadoc" > javadoc.zip
- JAVADOC_DIR="javadoc${DOCUMENTED_ARTIFACT//okhttp/}"
- mkdir $JAVADOC_DIR
- unzip javadoc.zip -d $JAVADOC_DIR
- rm javadoc.zip
-done
-
-# Download the 1.6.0 javadoc to '1.x/javadoc'.
-curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$ARTIFACT_ID&v=1.6.0&c=javadoc" > javadoc.zip
-mkdir -p 1.x/javadoc
-unzip javadoc.zip -d 1.x/javadoc
-rm javadoc.zip
-
# Stage all files in git and create a commit
git add .
git add -u
diff --git a/mockwebserver/README.md b/mockwebserver/README.md
index 3fd6ca6..ba40bf5 100644
--- a/mockwebserver/README.md
+++ b/mockwebserver/README.md
@@ -42,7 +42,7 @@
server.start();
// Ask the server for its URL. You'll need this to make HTTP requests.
- URL baseUrl = server.url("/v1/chat/");
+ HttpUrl baseUrl = server.url("/v1/chat/");
// Exercise your application code, which should make those HTTP requests.
// Responses are returned in the same order that they are enqueued.
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
index 9ef5211..7681ad1 100644
--- a/mockwebserver/pom.xml
+++ b/mockwebserver/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>mockwebserver</artifactId>
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java
new file mode 100644
index 0000000..2fff99c
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 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;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.UUID;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.X509Extensions;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
+
+/**
+ * A certificate and its private key. This can be used on the server side by HTTPS servers, or on
+ * the client side to verify those HTTPS servers. A held certificate can also be used to sign other
+ * held certificates, as done in practice by certificate authorities.
+ */
+public final class HeldCertificate {
+ public final X509Certificate certificate;
+ public final KeyPair keyPair;
+
+ public HeldCertificate(X509Certificate certificate, KeyPair keyPair) {
+ this.certificate = certificate;
+ this.keyPair = keyPair;
+ }
+
+ public static final class Builder {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private final long duration = 1000L * 60 * 60 * 24; // One day.
+ private String hostname;
+ private String serialNumber = "1";
+ private KeyPair keyPair;
+ private HeldCertificate issuedBy;
+ private int maxIntermediateCas;
+
+ public Builder serialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ return this;
+ }
+
+ /**
+ * Set this certificate's name. Typically this is the URL hostname for TLS certificates. This is
+ * the CN (common name) in the certificate. Will be a random string if no value is provided.
+ */
+ public Builder commonName(String hostname) {
+ this.hostname = hostname;
+ return this;
+ }
+
+ public Builder keyPair(KeyPair keyPair) {
+ this.keyPair = keyPair;
+ return this;
+ }
+
+ /**
+ * Set the certificate that signs this certificate. If unset, a self-signed certificate will be
+ * generated.
+ */
+ public Builder issuedBy(HeldCertificate signedBy) {
+ this.issuedBy = signedBy;
+ return this;
+ }
+
+ /**
+ * Set this certificate to be a certificate authority, with up to {@code maxIntermediateCas}
+ * intermediate certificate authorities beneath it.
+ */
+ public Builder ca(int maxIntermediateCas) {
+ this.maxIntermediateCas = maxIntermediateCas;
+ return this;
+ }
+
+ public HeldCertificate build() throws GeneralSecurityException {
+ // Subject, public & private keys for this certificate.
+ KeyPair heldKeyPair = keyPair != null
+ ? keyPair
+ : generateKeyPair();
+ X500Principal subject = hostname != null
+ ? new X500Principal("CN=" + hostname)
+ : new X500Principal("CN=" + UUID.randomUUID());
+
+ // Subject, public & private keys for this certificate's signer. It may be self signed!
+ KeyPair signedByKeyPair;
+ X500Principal signedByPrincipal;
+ if (issuedBy != null) {
+ signedByKeyPair = issuedBy.keyPair;
+ signedByPrincipal = issuedBy.certificate.getSubjectX500Principal();
+ } else {
+ signedByKeyPair = heldKeyPair;
+ signedByPrincipal = subject;
+ }
+
+ // Generate & sign the certificate.
+ long now = System.currentTimeMillis();
+ X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
+ generator.setSerialNumber(new BigInteger(serialNumber));
+ generator.setIssuerDN(signedByPrincipal);
+ generator.setNotBefore(new Date(now));
+ generator.setNotAfter(new Date(now + duration));
+ generator.setSubjectDN(subject);
+ generator.setPublicKey(heldKeyPair.getPublic());
+ generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+ if (maxIntermediateCas > 0) {
+ generator.addExtension(X509Extensions.BasicConstraints, true,
+ new BasicConstraints(maxIntermediateCas));
+ }
+
+ X509Certificate certificate = generator.generateX509Certificate(
+ signedByKeyPair.getPrivate(), "BC");
+ return new HeldCertificate(certificate, heldKeyPair);
+ }
+
+ public KeyPair generateKeyPair() throws GeneralSecurityException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+ keyPairGenerator.initialize(1024, new SecureRandom());
+ return keyPairGenerator.generateKeyPair();
+ }
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
index 546d660..fd1d020 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -18,24 +18,18 @@
import java.io.IOException;
import java.io.InputStream;
-import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
-import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
-import java.util.Date;
+import java.util.ArrayList;
+import java.util.List;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
-import javax.security.auth.x500.X500Principal;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.x509.X509V3CertificateGenerator;
/**
* Constructs an SSL context for testing. This uses Bouncy Castle to generate a
@@ -45,51 +39,70 @@
* reuse SSL context instances where possible.
*/
public final class SslContextBuilder {
- static {
- Security.addProvider(new BouncyCastleProvider());
- }
-
- private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
private static SSLContext localhost; // Lazily initialized.
- 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;
- }
-
/** Returns a new SSL context for this host's current localhost address. */
public static synchronized SSLContext localhost() {
- if (localhost == null) {
- try {
- localhost = new SslContextBuilder(InetAddress.getByName("localhost").getHostName()).build();
- } catch (GeneralSecurityException e) {
- throw new RuntimeException(e);
- } catch (UnknownHostException e) {
- throw new RuntimeException(e);
- }
+ if (localhost != null) return localhost;
+
+ try {
+ // Generate a self-signed cert for the server to serve and the client to trust.
+ HeldCertificate heldCertificate = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .commonName(InetAddress.getByName("localhost").getHostName())
+ .build();
+
+ localhost = new SslContextBuilder()
+ .certificateChain(heldCertificate)
+ .addTrustedCertificate(heldCertificate.certificate)
+ .build();
+
+ return localhost;
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
}
- return localhost;
+ }
+
+ private HeldCertificate[] chain;
+ private List<X509Certificate> trustedCertificates = new ArrayList<>();
+
+ /**
+ * Configure the certificate chain to use when serving HTTPS responses. The first certificate
+ * in this chain is the server's certificate, further certificates are included in the handshake
+ * so the client can build a trusted path to a CA certificate.
+ */
+ public SslContextBuilder certificateChain(HeldCertificate... chain) {
+ this.chain = chain;
+ return this;
+ }
+
+ /**
+ * Add a certificate authority that this client trusts. Servers that provide certificate chains
+ * signed by these roots (or their intermediates) will be accepted.
+ */
+ public SslContextBuilder addTrustedCertificate(X509Certificate certificate) {
+ trustedCertificates.add(certificate);
+ return this;
}
public SSLContext build() throws GeneralSecurityException {
+ // Put the certificate in a key store.
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, "1");
-
- // Put 'em in a key store.
KeyStore keyStore = newEmptyKeyStore(password);
- Certificate[] certificateChain = { certificate };
- keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
- keyStore.setCertificateEntry("cert", certificate);
+
+ if (chain != null) {
+ Certificate[] certificates = new Certificate[chain.length];
+ for (int i = 0; i < chain.length; i++) {
+ certificates[i] = chain[i].certificate;
+ }
+ keyStore.setKeyEntry("private", chain[0].keyPair.getPrivate(), password, certificates);
+ }
+
+ for (int i = 0; i < trustedCertificates.size(); i++) {
+ keyStore.setCertificateEntry("cert_" + i, trustedCertificates.get(i));
+ }
// Wrap it up in an SSL context.
KeyManagerFactory keyManagerFactory =
@@ -104,32 +117,6 @@
return sslContext;
}
- public 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.
- public X509Certificate selfSignedCertificate(KeyPair keyPair, String serialNumber)
- throws GeneralSecurityException {
- X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
- X500Principal issuer = new X500Principal("CN=" + hostName);
- X500Principal subject = new X500Principal("CN=" + hostName);
- generator.setSerialNumber(new BigInteger(serialNumber));
- 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());
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
index b95b64d..1574806 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
@@ -36,7 +36,7 @@
import okio.Source;
/** A basic SPDY/HTTP_2 server that serves the contents of a local directory. */
-public final class FramedServer implements IncomingStreamHandler {
+public final class FramedServer extends FramedConnection.Listener {
static final Logger logger = Logger.getLogger(FramedServer.class.getName());
private final List<Protocol> framedProtocols =
@@ -65,9 +65,10 @@
if (protocol == null || !framedProtocols.contains(protocol)) {
throw new ProtocolException("Protocol " + protocol + " unsupported");
}
- FramedConnection framedConnection = new FramedConnection.Builder(false, sslSocket)
+ FramedConnection framedConnection = new FramedConnection.Builder(false)
+ .socket(sslSocket)
.protocol(protocol)
- .handler(this)
+ .listener(this)
.build();
framedConnection.sendConnectionPreface();
} catch (IOException e) {
@@ -89,7 +90,7 @@
return sslSocket;
}
- @Override public void receive(final FramedStream stream) throws IOException {
+ @Override public void onStream(final FramedStream stream) throws IOException {
try {
List<Header> requestHeaders = stream.getRequestHeaders();
String path = null;
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
index bc43bd4..db68595 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -17,6 +17,7 @@
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.framed.Settings;
import com.squareup.okhttp.ws.WebSocketListener;
import java.util.ArrayList;
import java.util.List;
@@ -42,6 +43,7 @@
private TimeUnit bodyDelayUnit = TimeUnit.MILLISECONDS;
private List<PushPromise> promises = new ArrayList<>();
+ private Settings settings;
private WebSocketListener webSocketListener;
/** Creates a new mock response with an empty body. */
@@ -242,6 +244,20 @@
}
/**
+ * When {@linkplain MockWebServer#setProtocols(java.util.List) protocols}
+ * include {@linkplain com.squareup.okhttp.Protocol#HTTP_2 HTTP/2}, this
+ * pushes {@code settings} before writing the response.
+ */
+ public MockResponse withSettings(Settings settings) {
+ this.settings = settings;
+ return this;
+ }
+
+ public Settings getSettings() {
+ return settings;
+ }
+
+ /**
* Attempts to perform a web socket upgrade on the connection. This will overwrite any previously
* set status or body.
*/
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
index 0e746d3..2c76398 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -29,7 +29,7 @@
import com.squareup.okhttp.internal.framed.FramedConnection;
import com.squareup.okhttp.internal.framed.FramedStream;
import com.squareup.okhttp.internal.framed.Header;
-import com.squareup.okhttp.internal.framed.IncomingStreamHandler;
+import com.squareup.okhttp.internal.framed.Settings;
import com.squareup.okhttp.internal.http.HttpMethod;
import com.squareup.okhttp.internal.ws.RealWebSocket;
import com.squareup.okhttp.internal.ws.WebSocketProtocol;
@@ -82,6 +82,7 @@
import org.junit.runners.model.Statement;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_REQUEST_BODY;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -174,8 +175,10 @@
}
public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
- if (executor != null) throw new IllegalStateException(
- "setServerSocketFactory() must be called before start()");
+ if (executor != null) {
+ throw new IllegalStateException(
+ "setServerSocketFactory() must be called before start()");
+ }
this.serverSocketFactory = serverSocketFactory;
}
@@ -463,11 +466,12 @@
}
if (protocol != Protocol.HTTP_1_1) {
- FramedSocketHandler framedSocketHandler = new FramedSocketHandler(socket, protocol);
- FramedConnection framedConnection =
- new FramedConnection.Builder(false, socket).protocol(protocol)
- .handler(framedSocketHandler)
- .build();
+ FramedSocketHandler framedSocketListener = new FramedSocketHandler(socket, protocol);
+ FramedConnection framedConnection = new FramedConnection.Builder(false)
+ .socket(socket)
+ .protocol(protocol)
+ .listener(framedSocketListener)
+ .build();
openFramedConnections.add(framedConnection);
openClientSockets.remove(socket);
return;
@@ -672,7 +676,7 @@
final RealWebSocket webSocket =
new RealWebSocket(false /* is server */, source, sink, new SecureRandom(), replyExecutor,
listener, request.getPath()) {
- @Override protected void closeConnection() throws IOException {
+ @Override protected void close() throws IOException {
connectionClose.countDown();
}
};
@@ -704,6 +708,7 @@
throw new RuntimeException(e);
}
+ replyExecutor.shutdown();
Util.closeQuietly(sink);
Util.closeQuietly(source);
}
@@ -754,8 +759,9 @@
long periodDelayMs = policy.getThrottlePeriod(TimeUnit.MILLISECONDS);
long halfByteCount = byteCount / 2;
- boolean disconnectHalfway =
- !isRequest && policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
+ boolean disconnectHalfway = isRequest
+ ? policy.getSocketPolicy() == DISCONNECT_DURING_REQUEST_BODY
+ : policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
while (!socket.isClosed()) {
for (int b = 0; b < bytesPerPeriod; ) {
@@ -847,7 +853,7 @@
}
/** Processes HTTP requests layered over framed protocols. */
- private class FramedSocketHandler implements IncomingStreamHandler {
+ private class FramedSocketHandler extends FramedConnection.Listener {
private final Socket socket;
private final Protocol protocol;
private final AtomicInteger sequenceNumber = new AtomicInteger();
@@ -857,7 +863,7 @@
this.protocol = protocol;
}
- @Override public void receive(FramedStream stream) throws IOException {
+ @Override public void onStream(FramedStream stream) throws IOException {
RecordedRequest request = readRequest(stream);
requestQueue.add(request);
MockResponse response;
@@ -888,8 +894,14 @@
path = value;
} else if (name.equals(Header.VERSION)) {
version = value;
- } else {
+ } else if (protocol == Protocol.SPDY_3) {
+ for (String s : value.split("\u0000", -1)) {
+ httpHeaders.add(name.utf8(), s);
+ }
+ } else if (protocol == Protocol.HTTP_2) {
httpHeaders.add(name.utf8(), value);
+ } else {
+ throw new IllegalStateException();
}
}
@@ -904,6 +916,11 @@
}
private void writeResponse(FramedStream stream, MockResponse response) throws IOException {
+ Settings settings = response.getSettings();
+ if (settings != null) {
+ stream.getConnection().setSettings(settings);
+ }
+
if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
return;
}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
index c9c206c..bb36cdf 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
@@ -18,12 +18,14 @@
import java.net.HttpURLConnection;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Logger;
/**
* Default dispatcher that processes a script of responses. Populate the script
* by calling {@link #enqueueResponse(MockResponse)}.
*/
public class QueueDispatcher extends Dispatcher {
+ private static final Logger logger = Logger.getLogger(QueueDispatcher.class.getName());
protected final BlockingQueue<MockResponse> responseQueue = new LinkedBlockingQueue<>();
private MockResponse failFastResponse;
@@ -31,7 +33,7 @@
// To permit interactive/browser testing, ignore requests for favicons.
final String requestLine = request.getRequestLine();
if (requestLine != null && requestLine.equals("GET /favicon.ico HTTP/1.1")) {
- System.out.println("served " + requestLine);
+ logger.info("served " + requestLine);
return new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND);
}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
index 4583621..f71f33d 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -50,6 +50,9 @@
*/
DISCONNECT_AFTER_REQUEST,
+ /** Close connection after reading half of the request body (if present). */
+ DISCONNECT_DURING_REQUEST_BODY,
+
/** Close connection after writing half of the response body (if present). */
DISCONNECT_DURING_RESPONSE_BODY,
@@ -69,7 +72,7 @@
SHUTDOWN_OUTPUT_AT_END,
/**
- * Don't response to the request but keep the socket open. For testing
+ * Don't respond to the request but keep the socket open. For testing
* read response header timeout issue.
*/
NO_RESPONSE
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
index 0f757dd..1682dfe 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -20,6 +20,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
@@ -253,7 +254,33 @@
in.close();
}
- @Test public void disconnectHalfway() throws IOException {
+ @Test public void disconnectRequestHalfway() throws IOException {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY));
+ // Limit the size of the request body that the server holds in memory to an arbitrary
+ // 3.5 MBytes so this test can pass on devices with little memory.
+ server.setBodyLimit(7 * 512 * 1024);
+
+ HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ connection.setFixedLengthStreamingMode(1024 * 1024 * 1024); // 1 GB
+ connection.connect();
+ OutputStream out = connection.getOutputStream();
+
+ byte[] data = new byte[1024 * 1024];
+ int i;
+ for (i = 0; i < 1024; i++) {
+ try {
+ out.write(data);
+ out.flush();
+ } catch (IOException e) {
+ break;
+ }
+ }
+ assertEquals(512f, i, 10f); // Halfway +/- 1%
+ }
+
+ @Test public void disconnectResponseHalfway() throws IOException {
server.enqueue(new MockResponse()
.setBody("ab")
.setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
diff --git a/okcurl/pom.xml b/okcurl/pom.xml
index e80d121..fa585b8 100644
--- a/okcurl/pom.xml
+++ b/okcurl/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okcurl</artifactId>
diff --git a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
index dbc51f3..65c2cd0 100644
--- a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
+++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
@@ -203,7 +203,7 @@
}
String bodyData = data;
- String mimeType = "application/x-form-urlencoded";
+ String mimeType = "application/x-www-form-urlencoded";
if (headers != null) {
for (String header : headers) {
String[] parts = header.split(":", -1);
diff --git a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
index 0e2e3ae..d6961e1 100644
--- a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
+++ b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
@@ -45,7 +45,7 @@
RequestBody body = request.body();
assertEquals("POST", request.method());
assertEquals("http://example.com/", request.urlString());
- assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
+ assertEquals("application/x-www-form-urlencoded; charset=utf-8", body.contentType().toString());
assertEquals("foo", bodyAsString(body));
}
@@ -54,7 +54,7 @@
RequestBody body = request.body();
assertEquals("PUT", request.method());
assertEquals("http://example.com/", request.urlString());
- assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
+ assertEquals("application/x-www-form-urlencoded; charset=utf-8", body.contentType().toString());
assertEquals("foo", bodyAsString(body));
}
diff --git a/okhttp-android-support/pom.xml b/okhttp-android-support/pom.xml
index f514808..0cb0f7e 100644
--- a/okhttp-android-support/pom.xml
+++ b/okhttp-android-support/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-android-support</artifactId>
diff --git a/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
index 1d956b2..89570cc 100644
--- a/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
@@ -821,21 +821,17 @@
throw throwRequestSslAccessException();
}
- // ANDROID-BEGIN
- // @Override public long getContentLengthLong() {
- // return delegate.getContentLengthLong();
- // }
- // ANDROID-END
+ @Override public long getContentLengthLong() {
+ return delegate.getContentLengthLong();
+ }
@Override public void setFixedLengthStreamingMode(long contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
- // ANDROID-BEGIN
- // @Override public long getHeaderFieldLong(String field, long defaultValue) {
- // return delegate.getHeaderFieldLong(field, defaultValue);
- // }
- // ANDROID-END
+ @Override public long getHeaderFieldLong(String field, long defaultValue) {
+ return delegate.getHeaderFieldLong(field, defaultValue);
+ }
}
private static RuntimeException throwRequestModificationException() {
diff --git a/okhttp-apache/pom.xml b/okhttp-apache/pom.xml
index 74ff837..4da1a4e 100644
--- a/okhttp-apache/pom.xml
+++ b/okhttp-apache/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-apache</artifactId>
diff --git a/okhttp-hpacktests/src/test/resources/hpack-test-case b/okhttp-hpacktests/src/test/resources/hpack-test-case
new file mode 160000
index 0000000..a5652bc
--- /dev/null
+++ b/okhttp-hpacktests/src/test/resources/hpack-test-case
@@ -0,0 +1 @@
+Subproject commit a5652bc2bc3d2a992f39446369fb004a72e881d4
diff --git a/okhttp-logging-interceptor/README.md b/okhttp-logging-interceptor/README.md
new file mode 100644
index 0000000..a16bbd2
--- /dev/null
+++ b/okhttp-logging-interceptor/README.md
@@ -0,0 +1,49 @@
+Logging Interceptor
+===================
+
+An [OkHttp interceptor][1] which logs HTTP request and response data.
+
+```java
+OkHttpClient client = new OkHttpClient();
+HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
+logging.setLevel(Level.BASIC);
+client.interceptors().add(logging);
+```
+
+You can change the log level at any time by calling `setLevel`.
+
+To log to a custom location, pass a `Logger` instance to the constructor.
+```java
+HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new Logger() {
+ @Override public void log(String message) {
+ Timber.tag("OkHttp").d(message);
+ }
+});
+```
+
+**Warning**: The logs generated by this interceptor when using the `HEADERS` or `BODY` levels has
+the potential to leak sensitive information such as "Authorization" or "Cookie" headers and the
+contents of request and response bodies. This data should only be logged in a controlled way or in
+a non-production environment.
+
+
+Download
+--------
+
+Get via Maven:
+```xml
+<dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>logging-interceptor</artifactId>
+ <version>(insert latest version)</version>
+</dependency>
+```
+
+or via Gradle
+```groovy
+compile 'com.squareup.okhttp:logging-interceptor:(insert latest version)'
+```
+
+
+
+ [1]: https://github.com/square/okhttp/wiki/Interceptors
diff --git a/okhttp-logging-interceptor/pom.xml b/okhttp-logging-interceptor/pom.xml
new file mode 100644
index 0000000..efd552c
--- /dev/null
+++ b/okhttp-logging-interceptor/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>parent</artifactId>
+ <version>2.7.5</version>
+ </parent>
+
+ <artifactId>logging-interceptor</artifactId>
+ <name>OkHttp Logging Interceptor</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-testing-support</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java b/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java
new file mode 100644
index 0000000..402eee0
--- /dev/null
+++ b/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2015 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.logging;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.BufferedSource;
+
+/**
+ * An OkHttp interceptor which logs request and response information. Can be applied as an
+ * {@linkplain OkHttpClient#interceptors() application interceptor} or as a
+ * {@linkplain OkHttpClient#networkInterceptors() network interceptor}.
+ * <p>
+ * The format of the logs created by this class should not be considered stable and may change
+ * slightly between releases. If you need a stable logging format, use your own interceptor.
+ */
+public final class HttpLoggingInterceptor implements Interceptor {
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+
+ public enum Level {
+ /** No logs. */
+ NONE,
+ /**
+ * Logs request and response lines.
+ * <p>
+ * Example:
+ * <pre>{@code
+ * --> POST /greeting HTTP/1.1 (3-byte body)
+ *
+ * <-- HTTP/1.1 200 OK (22ms, 6-byte body)
+ * }</pre>
+ */
+ BASIC,
+ /**
+ * Logs request and response lines and their respective headers.
+ * <p>
+ * Example:
+ * <pre>{@code
+ * --> POST /greeting HTTP/1.1
+ * Host: example.com
+ * Content-Type: plain/text
+ * Content-Length: 3
+ * --> END POST
+ *
+ * <-- HTTP/1.1 200 OK (22ms)
+ * Content-Type: plain/text
+ * Content-Length: 6
+ * <-- END HTTP
+ * }</pre>
+ */
+ HEADERS,
+ /**
+ * Logs request and response lines and their respective headers and bodies (if present).
+ * <p>
+ * Example:
+ * <pre>{@code
+ * --> POST /greeting HTTP/1.1
+ * Host: example.com
+ * Content-Type: plain/text
+ * Content-Length: 3
+ *
+ * Hi?
+ * --> END GET
+ *
+ * <-- HTTP/1.1 200 OK (22ms)
+ * Content-Type: plain/text
+ * Content-Length: 6
+ *
+ * Hello!
+ * <-- END HTTP
+ * }</pre>
+ */
+ BODY
+ }
+
+ public interface Logger {
+ void log(String message);
+
+ /** A {@link Logger} defaults output appropriate for the current platform. */
+ Logger DEFAULT = new Logger() {
+ @Override public void log(String message) {
+ Platform.get().log(message);
+ }
+ };
+ }
+
+ public HttpLoggingInterceptor() {
+ this(Logger.DEFAULT);
+ }
+
+ public HttpLoggingInterceptor(Logger logger) {
+ this.logger = logger;
+ }
+
+ private final Logger logger;
+
+ private volatile Level level = Level.NONE;
+
+ /** Change the level at which this interceptor logs. */
+ public HttpLoggingInterceptor setLevel(Level level) {
+ if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
+ this.level = level;
+ return this;
+ }
+
+ public Level getLevel() {
+ return level;
+ }
+
+ @Override public Response intercept(Chain chain) throws IOException {
+ Level level = this.level;
+
+ Request request = chain.request();
+ if (level == Level.NONE) {
+ return chain.proceed(request);
+ }
+
+ boolean logBody = level == Level.BODY;
+ boolean logHeaders = logBody || level == Level.HEADERS;
+
+ RequestBody requestBody = request.body();
+ boolean hasRequestBody = requestBody != null;
+
+ Connection connection = chain.connection();
+ Protocol protocol = connection != null ? connection.getProtocol() : Protocol.HTTP_1_1;
+ String requestStartMessage =
+ "--> " + request.method() + ' ' + request.httpUrl() + ' ' + protocol(protocol);
+ if (!logHeaders && hasRequestBody) {
+ requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
+ }
+ logger.log(requestStartMessage);
+
+ if (logHeaders) {
+ if (hasRequestBody) {
+ // Request body headers are only present when installed as a network interceptor. Force
+ // them to be included (when available) so there values are known.
+ if (requestBody.contentType() != null) {
+ logger.log("Content-Type: " + requestBody.contentType());
+ }
+ if (requestBody.contentLength() != -1) {
+ logger.log("Content-Length: " + requestBody.contentLength());
+ }
+ }
+
+ Headers headers = request.headers();
+ for (int i = 0, count = headers.size(); i < count; i++) {
+ String name = headers.name(i);
+ // Skip headers from the request body as they are explicitly logged above.
+ if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
+ logger.log(name + ": " + headers.value(i));
+ }
+ }
+
+ if (!logBody || !hasRequestBody) {
+ logger.log("--> END " + request.method());
+ } else if (bodyEncoded(request.headers())) {
+ logger.log("--> END " + request.method() + " (encoded body omitted)");
+ } else {
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+
+ Charset charset = UTF8;
+ MediaType contentType = requestBody.contentType();
+ if (contentType != null) {
+ contentType.charset(UTF8);
+ }
+
+ logger.log("");
+ logger.log(buffer.readString(charset));
+
+ logger.log("--> END " + request.method()
+ + " (" + requestBody.contentLength() + "-byte body)");
+ }
+ }
+
+ long startNs = System.nanoTime();
+ Response response = chain.proceed(request);
+ long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
+
+ ResponseBody responseBody = response.body();
+ logger.log("<-- " + protocol(response.protocol()) + ' ' + response.code() + ' '
+ + response.message() + " (" + tookMs + "ms"
+ + (!logHeaders ? ", " + responseBody.contentLength() + "-byte body" : "") + ')');
+
+ if (logHeaders) {
+ Headers headers = response.headers();
+ for (int i = 0, count = headers.size(); i < count; i++) {
+ logger.log(headers.name(i) + ": " + headers.value(i));
+ }
+
+ if (!logBody || !HttpEngine.hasBody(response)) {
+ logger.log("<-- END HTTP");
+ } else if (bodyEncoded(response.headers())) {
+ logger.log("<-- END HTTP (encoded body omitted)");
+ } else {
+ BufferedSource source = responseBody.source();
+ source.request(Long.MAX_VALUE); // Buffer the entire body.
+ Buffer buffer = source.buffer();
+
+ Charset charset = UTF8;
+ MediaType contentType = responseBody.contentType();
+ if (contentType != null) {
+ charset = contentType.charset(UTF8);
+ }
+
+ if (responseBody.contentLength() != 0) {
+ logger.log("");
+ logger.log(buffer.clone().readString(charset));
+ }
+
+ logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
+ }
+ }
+
+ return response;
+ }
+
+ private boolean bodyEncoded(Headers headers) {
+ String contentEncoding = headers.get("Content-Encoding");
+ return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
+ }
+
+ private static String protocol(Protocol protocol) {
+ return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
+ }
+}
diff --git a/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java b/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java
new file mode 100644
index 0000000..dbd1e84
--- /dev/null
+++ b/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2015 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.logging;
+
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.logging.HttpLoggingInterceptor.Level;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ByteString;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class HttpLoggingInterceptorTest {
+ private static final MediaType PLAIN = MediaType.parse("text/plain; charset=utf-8");
+
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private final OkHttpClient client = new OkHttpClient();
+ private String host;
+ private HttpUrl url;
+
+ private final LogRecorder networkLogs = new LogRecorder();
+ private final HttpLoggingInterceptor networkInterceptor =
+ new HttpLoggingInterceptor(networkLogs);
+
+ private final LogRecorder applicationLogs = new LogRecorder();
+ private final HttpLoggingInterceptor applicationInterceptor =
+ new HttpLoggingInterceptor(applicationLogs);
+
+ private void setLevel(Level level) {
+ networkInterceptor.setLevel(level);
+ applicationInterceptor.setLevel(level);
+ }
+
+ @Before public void setUp() {
+ client.networkInterceptors().add(networkInterceptor);
+ client.interceptors().add(applicationInterceptor);
+ client.setConnectionPool(null);
+
+ host = server.getHostName() + ":" + server.getPort();
+ url = server.url("/");
+ }
+
+ @Test public void levelGetter() {
+ // The default is NONE.
+ assertEquals(Level.NONE, applicationInterceptor.getLevel());
+
+ for (Level level : Level.values()) {
+ applicationInterceptor.setLevel(level);
+ assertEquals(level, applicationInterceptor.getLevel());
+ }
+ }
+
+ @Test public void setLevelShouldPreventNullValue() {
+ try {
+ applicationInterceptor.setLevel(null);
+ fail();
+ } catch (NullPointerException expected) {
+ assertEquals("level == null. Use Level.NONE instead.", expected.getMessage());
+ }
+ }
+
+ @Test public void setLevelShouldReturnSameInstanceOfInterceptor() {
+ for (Level level : Level.values()) {
+ assertSame(applicationInterceptor, applicationInterceptor.setLevel(level));
+ }
+ }
+
+ @Test public void none() throws IOException {
+ server.enqueue(new MockResponse());
+ client.newCall(request().build()).execute();
+
+ applicationLogs.assertNoMoreLogs();
+ networkLogs.assertNoMoreLogs();
+ }
+
+ @Test public void basicGet() throws IOException {
+ setLevel(Level.BASIC);
+
+ server.enqueue(new MockResponse());
+ client.newCall(request().build()).execute();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void basicPost() throws IOException {
+ setLevel(Level.BASIC);
+
+ server.enqueue(new MockResponse());
+ client.newCall(request().post(RequestBody.create(PLAIN, "Hi?")).build()).execute();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1 (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1 (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void basicResponseBody() throws IOException {
+ setLevel(Level.BASIC);
+
+ server.enqueue(new MockResponse()
+ .setBody("Hello!")
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 6-byte body\\)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 6-byte body\\)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersGet() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersPost() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ Request request = request().post(RequestBody.create(PLAIN, "Hi?")).build();
+ Response response = client.newCall(request).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersPostNoContentType() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ Request request = request().post(RequestBody.create(null, "Hi?")).build();
+ Response response = client.newCall(request).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersPostNoLength() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ RequestBody body = new RequestBody() {
+ @Override public MediaType contentType() {
+ return PLAIN;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Hi!");
+ }
+ };
+ Response response = client.newCall(request().post(body).build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Transfer-Encoding: chunked")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersResponseBody() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse()
+ .setBody("Hello!")
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyGet() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse());
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyGet204() throws IOException {
+ setLevel(Level.BODY);
+ bodyGetNoBody(204);
+ }
+
+ @Test public void bodyGet205() throws IOException {
+ setLevel(Level.BODY);
+ bodyGetNoBody(205);
+ }
+
+ private void bodyGetNoBody(int code) throws IOException {
+ server.enqueue(new MockResponse()
+ .setStatus("HTTP/1.1 " + code + " No Content"));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 " + code + " No Content \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 " + code + " No Content \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyPost() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse());
+ Request request = request().post(RequestBody.create(PLAIN, "Hi?")).build();
+ Response response = client.newCall(request).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("")
+ .assertLogEqual("Hi?")
+ .assertLogEqual("--> END POST (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("")
+ .assertLogEqual("Hi?")
+ .assertLogEqual("--> END POST (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyResponseBody() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse()
+ .setBody("Hello!")
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyResponseBodyChunked() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse()
+ .setChunkedBody("Hello!", 2)
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Transfer-encoding: chunked")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Transfer-encoding: chunked")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyResponseNotIdentityEncoded() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse()
+ .setHeader("Content-Encoding", "gzip")
+ .setHeader("Content-Type", PLAIN)
+ .setBody(new Buffer().write(ByteString.decodeBase64(
+ "H4sIAAAAAAAAAPNIzcnJ11HwQKIAdyO+9hMAAAA="))));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Encoding: gzip")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("Content-Length: \\d+")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (encoded body omitted)")
+ .assertNoMoreLogs();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello, Hello, Hello")
+ .assertLogEqual("<-- END HTTP (19-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ private Request.Builder request() {
+ return new Request.Builder().url(url);
+ }
+
+ private static class LogRecorder implements HttpLoggingInterceptor.Logger {
+ private final List<String> logs = new ArrayList<>();
+ private int index;
+
+ LogRecorder assertLogEqual(String expected) {
+ assertTrue("No more messages found", index < logs.size());
+ String actual = logs.get(index++);
+ assertEquals(expected, actual);
+ return this;
+ }
+
+ LogRecorder assertLogMatch(String pattern) {
+ assertTrue("No more messages found", index < logs.size());
+ String actual = logs.get(index++);
+ assertTrue("<" + actual + "> did not match pattern <" + pattern + ">",
+ Pattern.matches(pattern, actual));
+ return this;
+ }
+
+ void assertNoMoreLogs() {
+ assertTrue("More messages remain: " + logs.subList(index, logs.size()), index == logs.size());
+ }
+
+ @Override public void log(String message) {
+ logs.add(message);
+ }
+ }
+}
diff --git a/okhttp-testing-support/pom.xml b/okhttp-testing-support/pom.xml
index 654b0e3..42b4701 100644
--- a/okhttp-testing-support/pom.xml
+++ b/okhttp-testing-support/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-testing-support</artifactId>
diff --git a/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
index 3a043cb..6498fe8 100644
--- a/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
+++ b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
@@ -18,32 +18,95 @@
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import okio.Buffer;
+import okio.ForwardingSink;
+import okio.ForwardingSource;
import okio.Sink;
import okio.Source;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
/** A simple file system where all files are held in memory. Not safe for concurrent use. */
-public final class InMemoryFileSystem implements FileSystem {
+public final class InMemoryFileSystem implements FileSystem, TestRule {
private final Map<File, Buffer> files = new LinkedHashMap<>();
+ private final Map<Source, File> openSources = new IdentityHashMap<>();
+ private final Map<Sink, File> openSinks = new IdentityHashMap<>();
+
+ @Override public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override public void evaluate() throws Throwable {
+ base.evaluate();
+ ensureResourcesClosed();
+ }
+ };
+ }
+
+ public void ensureResourcesClosed() {
+ List<String> openResources = new ArrayList<>();
+ for (File file : openSources.values()) {
+ openResources.add("Source for " + file);
+ }
+ for (File file : openSinks.values()) {
+ openResources.add("Sink for " + file);
+ }
+ if (!openResources.isEmpty()) {
+ StringBuilder builder = new StringBuilder("Resources acquired but not closed:");
+ for (String resource : openResources) {
+ builder.append("\n * ").append(resource);
+ }
+ throw new IllegalStateException(builder.toString());
+ }
+ }
@Override public Source source(File file) throws FileNotFoundException {
Buffer result = files.get(file);
if (result == null) throw new FileNotFoundException();
- return result.clone();
+
+ final Source source = result.clone();
+ openSources.put(source, file);
+
+ return new ForwardingSource(source) {
+ @Override public void close() throws IOException {
+ openSources.remove(source);
+ super.close();
+ }
+ };
}
@Override public Sink sink(File file) throws FileNotFoundException {
- Buffer result = new Buffer();
- files.put(file, result);
- return result;
+ return sink(file, false);
}
@Override public Sink appendingSink(File file) throws FileNotFoundException {
- Buffer result = files.get(file);
- return result != null ? result : sink(file);
+ return sink(file, true);
+ }
+
+ private Sink sink(File file, boolean appending) {
+ Buffer result = null;
+ if (appending) {
+ result = files.get(file);
+ }
+ if (result == null) {
+ result = new Buffer();
+ }
+ files.put(file, result);
+
+ final Sink sink = result;
+ openSinks.put(sink, file);
+
+ return new ForwardingSink(sink) {
+ @Override public void close() throws IOException {
+ openSinks.remove(sink);
+ super.close();
+ }
+ };
}
@Override public void delete(File file) throws IOException {
diff --git a/okhttp-tests/pom.xml b/okhttp-tests/pom.xml
index 2bb1982..911afd2 100644
--- a/okhttp-tests/pom.xml
+++ b/okhttp-tests/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-tests</artifactId>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
index 44c39a8..1e623f0 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
@@ -26,6 +26,7 @@
import static org.junit.Assert.assertFalse;
public final class AddressTest {
+ private Dns dns = Dns.SYSTEM;
private SocketFactory socketFactory = SocketFactory.getDefault();
private Authenticator authenticator = AuthenticatorAdapter.INSTANCE;
private List<Protocol> protocols = Util.immutableList(Protocol.HTTP_1_1);
@@ -33,18 +34,18 @@
private RecordingProxySelector proxySelector = new RecordingProxySelector();
@Test public void equalsAndHashcode() throws Exception {
- Address a = new Address("square.com", 80, socketFactory, null, null, null,
+ Address a = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, proxySelector);
- Address b = new Address("square.com", 80, socketFactory, null, null, null,
+ Address b = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, proxySelector);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
@Test public void differentProxySelectorsAreDifferent() throws Exception {
- Address a = new Address("square.com", 80, socketFactory, null, null, null,
+ Address a = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
- Address b = new Address("square.com", 80, socketFactory, null, null, null,
+ Address b = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
assertFalse(a.equals(b));
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
index b9e1d50..c762862 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
@@ -19,7 +19,6 @@
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.io.FileSystem;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -76,9 +75,9 @@
@Rule public MockWebServer server = new MockWebServer();
@Rule public MockWebServer server2 = new MockWebServer();
+ @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
private final SSLContext sslContext = SslContextBuilder.localhost();
- private final FileSystem fileSystem = new InMemoryFileSystem();
private final OkHttpClient client = new OkHttpClient();
private Cache cache;
private final CookieManager cookieManager = new CookieManager();
@@ -93,6 +92,7 @@
@After public void tearDown() throws Exception {
ResponseCache.setDefault(null);
CookieHandler.setDefault(null);
+ cache.delete();
}
/**
@@ -266,7 +266,7 @@
Principal localPrincipal = response1.handshake().localPrincipal();
Response response2 = client.newCall(request).execute(); // Cached!
- assertEquals("ABC", response2.body().source().readUtf8());
+ assertEquals("ABC", response2.body().string());
assertEquals(2, cache.getRequestCount());
assertEquals(1, cache.getNetworkCount());
@@ -462,6 +462,26 @@
assertEquals("b", get(url).body().string());
}
+ /** https://github.com/square/okhttp/issues/2198 */
+ @Test public void cachedRedirect() throws IOException {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Location: /bar"));
+ server.enqueue(new MockResponse()
+ .setBody("ABC"));
+ server.enqueue(new MockResponse()
+ .setBody("ABC"));
+
+ Request request1 = new Request.Builder().url(server.url("/")).build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("ABC", response1.body().string());
+
+ Request request2 = new Request.Builder().url(server.url("/")).build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("ABC", response2.body().string());
+ }
+
@Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
@@ -1007,7 +1027,7 @@
assertEquals("A", get(server.url("/")).body().string());
assertEquals("A", get(server.url("/")).body().string());
- assertEquals(1, client.getConnectionPool().getConnectionCount());
+ assertEquals(1, client.getConnectionPool().getIdleConnectionCount());
}
@Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -1873,6 +1893,7 @@
Response response = get(server.url("/"));
assertEquals("A", response.header(""));
+ assertEquals("body", response.body().string());
}
/**
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
index 051eae4..34ca42a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -15,14 +15,13 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.DoubleInetAddressNetwork;
-import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.DoubleInetAddressDns;
import com.squareup.okhttp.internal.RecordingOkAuthenticator;
-import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SingleInetAddressDns;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.Version;
-import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.http.FakeDns;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.Dispatcher;
import com.squareup.okhttp.mockwebserver.MockResponse;
@@ -88,9 +87,9 @@
@Rule public final TestRule timeout = new Timeout(30_000);
@Rule public final MockWebServer server = new MockWebServer();
@Rule public final MockWebServer server2 = new MockWebServer();
+ @Rule public final InMemoryFileSystem fileSystem = new InMemoryFileSystem();
private SSLContext sslContext = SslContextBuilder.localhost();
- private FileSystem fileSystem = new InMemoryFileSystem();
private OkHttpClient client = new OkHttpClient();
private RecordingCallback callback = new RecordingCallback();
private TestLogHandler logHandler = new TestLogHandler();
@@ -172,14 +171,48 @@
.assertNotSuccessful();
}
+ @Test public void get_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ get();
+ }
+
+ @Test public void get_HTTPS() throws Exception {
+ enableTls();
+ get();
+ }
+
@Test public void get_SPDY_3() throws Exception {
enableProtocol(Protocol.SPDY_3);
get();
}
- @Test public void get_HTTP_2() throws Exception {
+ @Test public void repeatedHeaderNames() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("B", "123")
+ .addHeader("B", "234"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .addHeader("A", "345")
+ .addHeader("A", "456")
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertHeader("B", "123", "234");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals(Arrays.asList("345", "456"), recordedRequest.getHeaders().values("A"));
+ }
+
+ @Test public void repeatedHeaderNames_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ repeatedHeaderNames();
+ }
+
+ @Test public void repeatedHeaderNames_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
- get();
+ repeatedHeaderNames();
}
@Test public void getWithRequestBody() throws Exception {
@@ -212,8 +245,8 @@
assertNull(recordedRequest.getHeader("Content-Length"));
}
- @Test public void head_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void head_HTTPS() throws Exception {
+ enableTls();
head();
}
@@ -222,6 +255,11 @@
head();
}
+ @Test public void head_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ head();
+ }
+
@Test public void post() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -241,8 +279,8 @@
assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
}
- @Test public void post_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void post_HTTPS() throws Exception {
+ enableTls();
post();
}
@@ -251,6 +289,11 @@
post();
}
+ @Test public void post_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ post();
+ }
+
@Test public void postZeroLength() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -270,8 +313,8 @@
assertEquals(null, recordedRequest.getHeader("Content-Type"));
}
- @Test public void postZeroLength_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void postZerolength_HTTPS() throws Exception {
+ enableTls();
postZeroLength();
}
@@ -280,12 +323,17 @@
postZeroLength();
}
+ @Test public void postZeroLength_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postZeroLength();
+ }
+
@Test public void postBodyRetransmittedAfterAuthorizationFail() throws Exception {
postBodyRetransmittedAfterAuthorizationFail("abc");
}
- @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception {
+ enableTls();
postBodyRetransmittedAfterAuthorizationFail("abc");
}
@@ -294,13 +342,18 @@
postBodyRetransmittedAfterAuthorizationFail("abc");
}
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postBodyRetransmittedAfterAuthorizationFail("abc");
+ }
+
/** Don't explode when resending an empty post. https://github.com/square/okhttp/issues/1131 */
@Test public void postEmptyBodyRetransmittedAfterAuthorizationFail() throws Exception {
postBodyRetransmittedAfterAuthorizationFail("");
}
- @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception {
+ enableTls();
postBodyRetransmittedAfterAuthorizationFail("");
}
@@ -309,6 +362,11 @@
postBodyRetransmittedAfterAuthorizationFail("");
}
+ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postBodyRetransmittedAfterAuthorizationFail("");
+ }
+
private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exception {
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse());
@@ -385,8 +443,8 @@
assertEquals(null, recordedRequest.getHeader("Content-Type"));
}
- @Test public void delete_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void delete_HTTPS() throws Exception {
+ enableTls();
delete();
}
@@ -395,6 +453,11 @@
delete();
}
+ @Test public void delete_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ delete();
+ }
+
@Test public void deleteWithRequestBody() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -431,8 +494,8 @@
assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
}
- @Test public void put_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void put_HTTPS() throws Exception {
+ enableTls();
put();
}
@@ -441,6 +504,11 @@
put();
}
+ @Test public void put_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ put();
+ }
+
@Test public void patch() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -460,13 +528,18 @@
assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
}
- @Test public void patch_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void patch_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
patch();
}
- @Test public void patch_HTTP_2() throws Exception {
- enableProtocol(Protocol.HTTP_2);
+ @Test public void patch_HTTPS() throws Exception {
+ enableTls();
+ patch();
+ }
+
+ @Test public void patch_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
patch();
}
@@ -497,7 +570,8 @@
.build();
Call call = client.newCall(request);
- call.execute();
+ Response response = call.execute();
+ response.body().close();
try {
call.execute();
@@ -675,17 +749,19 @@
long elapsedNanos = System.nanoTime() - startNanos;
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
assertTrue(String.format("Timed out: %sms", elapsedMillis), elapsedMillis < 500);
+ } finally {
+ bodySource.close();
}
}
- // https://github.com/square/okhttp/issues/442
+ /** https://github.com/square/okhttp/issues/442 */
@Test public void timeoutsNotRetried() throws Exception {
server.enqueue(new MockResponse()
.setSocketPolicy(SocketPolicy.NO_RESPONSE));
server.enqueue(new MockResponse()
.setBody("unreachable!"));
- Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setDns(new DoubleInetAddressDns());
client.setReadTimeout(100, TimeUnit.MILLISECONDS);
Request request = new Request.Builder().url(server.url("/")).build();
@@ -697,6 +773,20 @@
}
}
+ /** https://github.com/square/okhttp/issues/1801 */
+ @Test public void asyncCallEngineInitialized() throws Exception {
+ OkHttpClient c = new OkHttpClient();
+ c.interceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ throw new IOException();
+ }
+ });
+ Request request = new Request.Builder().url(server.url("/")).build();
+ c.newCall(request).enqueue(callback);
+ RecordedResponse response = callback.await(request.httpUrl());
+ assertEquals(request, response.request);
+ }
+
@Test public void reusedSinksGetIndependentTimeoutInstances() throws Exception {
server.enqueue(new MockResponse());
server.enqueue(new MockResponse());
@@ -764,27 +854,21 @@
}
@Test public void tls() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse()
.setBody("abc")
.addHeader("Content-Type: text/plain"));
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
executeSynchronously(new Request.Builder().url(server.url("/")).build())
.assertHandshake();
}
@Test public void tls_Async() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse()
.setBody("abc")
.addHeader("Content-Type: text/plain"));
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
Request request = new Request.Builder()
.url(server.url("/"))
.build();
@@ -794,25 +878,28 @@
}
@Test public void recoverWhenRetryOnConnectionFailureIsTrue() throws Exception {
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse().setBody("seed connection pool"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
server.enqueue(new MockResponse().setBody("retry success"));
- Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setDns(new DoubleInetAddressDns());
assertTrue(client.getRetryOnConnectionFailure());
Request request = new Request.Builder().url(server.url("/")).build();
- Response response = client.newCall(request).execute();
- assertEquals("retry success", response.body().string());
+ executeSynchronously(request).assertBody("seed connection pool");
+ executeSynchronously(request).assertBody("retry success");
}
@Test public void noRecoverWhenRetryOnConnectionFailureIsFalse() throws Exception {
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse().setBody("seed connection pool"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
server.enqueue(new MockResponse().setBody("unreachable!"));
- Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setDns(new DoubleInetAddressDns());
client.setRetryOnConnectionFailure(false);
Request request = new Request.Builder().url(server.url("/")).build();
+ executeSynchronously(request).assertBody("seed connection pool");
try {
// If this succeeds, too many requests were made.
client.newCall(request).execute();
@@ -828,7 +915,7 @@
suppressTlsFallbackScsv(client);
client.setHostnameVerifier(new RecordingHostnameVerifier());
- Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+ client.setDns(new SingleInetAddressDns());
executeSynchronously(new Request.Builder().url(server.url("/")).build())
.assertBody("abc");
@@ -850,7 +937,7 @@
new RecordingSSLSocketFactory(sslContext.getSocketFactory());
client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
- Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+ client.setDns(new SingleInetAddressDns());
Request request = new Request.Builder().url(server.url("/")).build();
try {
@@ -890,7 +977,7 @@
suppressTlsFallbackScsv(client);
client.setHostnameVerifier(new RecordingHostnameVerifier());
- Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+ client.setDns(new SingleInetAddressDns());
Request request = new Request.Builder().url(server.url("/")).build();
try {
@@ -920,26 +1007,24 @@
}
@Test public void setFollowSslRedirectsFalse() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
- server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location: http://square.com"));
+ enableTls();
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: http://square.com"));
client.setFollowSslRedirects(false);
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
Request request = new Request.Builder().url(server.url("/")).build();
Response response = client.newCall(request).execute();
assertEquals(301, response.code());
+ response.body().close();
}
@Test public void matchingPinnedCertificate() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse());
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
// Make a first request without certificate pinning. Use it to collect certificates to pin.
Request request1 = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request1).execute();
@@ -947,21 +1032,20 @@
for (Certificate certificate : response1.handshake().peerCertificates()) {
certificatePinnerBuilder.add(server.getHostName(), CertificatePinner.pin(certificate));
}
+ response1.body().close();
// Make another request with certificate pinning. It should complete normally.
client.setCertificatePinner(certificatePinnerBuilder.build());
Request request2 = new Request.Builder().url(server.url("/")).build();
Response response2 = client.newCall(request2).execute();
assertNotSame(response2.handshake(), response1.handshake());
+ response2.body().close();
}
@Test public void unmatchingPinnedCertificate() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse());
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
// Pin publicobject.com's cert.
client.setCertificatePinner(new CertificatePinner.Builder()
.add(server.getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
@@ -1293,6 +1377,27 @@
assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
}
+ @Test public void propfindRedirectsToPropfind() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ .addHeader("Location: /page2")
+ .setBody("This page has moved!"));
+ server.enqueue(new MockResponse().setBody("Page 2"));
+
+ Response response = client.newCall(new Request.Builder()
+ .url(server.url("/page1"))
+ .method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), "Request Body"))
+ .build()).execute();
+ assertEquals("Page 2", response.body().string());
+
+ RecordedRequest page1 = server.takeRequest();
+ assertEquals("PROPFIND /page1 HTTP/1.1", page1.getRequestLine());
+ assertEquals("Request Body", page1.getBody().readUtf8());
+
+ RecordedRequest page2 = server.takeRequest();
+ assertEquals("PROPFIND /page2 HTTP/1.1", page2.getRequestLine());
+ }
+
@Test public void redirectsDoNotIncludeTooManyCookies() throws Exception {
server2.enqueue(new MockResponse().setBody("Page 2"));
server.enqueue(new MockResponse()
@@ -1511,13 +1616,13 @@
}
@Test public void cancelTagImmediatelyAfterEnqueue() throws Exception {
+ server.enqueue(new MockResponse());
Call call = client.newCall(new Request.Builder()
.url(server.url("/a"))
.tag("request")
.build());
call.enqueue(callback);
client.cancel("request");
- assertEquals(0, server.getRequestCount());
callback.await(server.url("/a")).assertFailure("Canceled");
}
@@ -1559,6 +1664,11 @@
}
}
+ @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTPS() throws Exception {
+ enableTls();
+ cancelInFlightBeforeResponseReadThrowsIOE();
+ }
+
@Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
cancelInFlightBeforeResponseReadThrowsIOE();
@@ -1596,6 +1706,11 @@
callback.await(requestB.httpUrl()).assertFailure("Canceled");
}
+ @Test public void canceledBeforeIOSignalsOnFailure_HTTPS() throws Exception {
+ enableTls();
+ canceledBeforeIOSignalsOnFailure();
+ }
+
@Test public void canceledBeforeIOSignalsOnFailure_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
canceledBeforeIOSignalsOnFailure();
@@ -1623,6 +1738,11 @@
"Socket closed");
}
+ @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTPS() throws Exception {
+ enableTls();
+ canceledBeforeResponseReadSignalsOnFailure();
+ }
+
@Test public void canceledBeforeResponseReadSignalsOnFailure_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
canceledBeforeResponseReadSignalsOnFailure();
@@ -1670,6 +1790,12 @@
assertFalse(failureRef.get());
}
+ @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTPS()
+ throws Exception {
+ enableTls();
+ canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce();
+ }
+
@Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTP_2()
throws Exception {
enableProtocol(Protocol.HTTP_2);
@@ -1729,6 +1855,22 @@
.assertRequestHeader("Accept-Encoding", "gzip");
}
+ /** https://github.com/square/okhttp/issues/1927 */
+ @Test public void gzipResponseAfterAuthenticationChallenge() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(401));
+ server.enqueue(new MockResponse()
+ .setBody(gzip("abcabcabc"))
+ .addHeader("Content-Encoding: gzip"));
+ client.setAuthenticator(new RecordingOkAuthenticator("password"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ executeSynchronously(request)
+ .assertBody("abcabcabc");
+ }
+
@Test public void asyncResponseCanBeConsumedLater() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
server.enqueue(new MockResponse().setBody("def"));
@@ -1842,6 +1984,178 @@
.assertHeader("", "ef");
}
+ @Test public void customDns() throws Exception {
+ // Configure a DNS that returns our MockWebServer for every hostname.
+ FakeDns dns = new FakeDns();
+ dns.addresses(Dns.SYSTEM.lookup(server.url("/").host()));
+ client.setDns(dns);
+
+ server.enqueue(new MockResponse());
+ Request request = new Request.Builder()
+ .url(server.url("/").newBuilder().host("android.com").build())
+ .build();
+ executeSynchronously(request).assertCode(200);
+
+ dns.assertRequests("android.com");
+ }
+
+ /** We had a bug where failed HTTP/2 calls could break the entire connection. */
+ @Test public void failingCallsDoNotInterfereWithConnection() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+
+ server.enqueue(new MockResponse().setBody("Response 1"));
+ server.enqueue(new MockResponse().setBody("Response 2"));
+
+ RequestBody requestBody = new RequestBody() {
+ @Override public MediaType contentType() {
+ return null;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("abc");
+ sink.flush();
+
+ makeFailingCall();
+
+ sink.writeUtf8("def");
+ sink.flush();
+ }
+ };
+ Call call = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .post(requestBody)
+ .build());
+ assertEquals("Response 1", call.execute().body().string());
+ }
+
+ /** Test which headers are sent unencrypted to the HTTP proxy. */
+ @Test public void proxyConnectOmitsApplicationHeaders() throws Exception {
+ 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"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setProxy(server.toProxyAddress());
+ RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+ client.setHostnameVerifier(hostnameVerifier);
+
+ Request request = new Request.Builder()
+ .url("https://android.com/foo")
+ .header("Private", "Secret")
+ .header("User-Agent", "App 1.0")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("encrypted response from the origin server", response.body().string());
+
+ RecordedRequest connect = server.takeRequest();
+ assertNull(connect.getHeader("Private"));
+ assertEquals(Version.userAgent(), connect.getHeader("User-Agent"));
+ assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
+ assertEquals("android.com", connect.getHeader("Host"));
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals("Secret", get.getHeader("Private"));
+ assertEquals("App 1.0", get.getHeader("User-Agent"));
+
+ assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+ }
+
+ /** Respond to a proxy authorization challenge. */
+ @Test public void proxyAuthenticateOnConnect() throws Exception {
+ 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("response body"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setProxy(server.toProxyAddress());
+ client.setAuthenticator(new RecordingOkAuthenticator("password"));
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ Request request = new Request.Builder()
+ .url("https://android.com/foo")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("response body", response.body().string());
+
+ RecordedRequest connect1 = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
+ assertNull(connect1.getHeader("Proxy-Authorization"));
+
+ RecordedRequest connect2 = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
+ assertEquals("password", connect2.getHeader("Proxy-Authorization"));
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+ assertNull(get.getHeader("Proxy-Authorization"));
+ }
+
+ /**
+ * Confirm that we don't send the Proxy-Authorization header from the request to the proxy server.
+ * We used to have that behavior but it is problematic because unrelated requests end up sharing
+ * credentials. Worse, that approach leaks proxy credentials to the origin server.
+ */
+ @Test public void noProactiveProxyAuthorization() throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), true);
+ server.enqueue(new MockResponse()
+ .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+ .clearHeaders());
+ server.enqueue(new MockResponse()
+ .setBody("response body"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setProxy(server.toProxyAddress());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ Request request = new Request.Builder()
+ .url("https://android.com/foo")
+ .header("Proxy-Authorization", "password")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("response body", response.body().string());
+
+ RecordedRequest connect = server.takeRequest();
+ assertNull(connect.getHeader("Proxy-Authorization"));
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals("password", get.getHeader("Proxy-Authorization"));
+ }
+
+ private void makeFailingCall() {
+ RequestBody requestBody = new RequestBody() {
+ @Override public MediaType contentType() {
+ return null;
+ }
+
+ @Override public long contentLength() throws IOException {
+ return 1;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ throw new IOException("write body fail!");
+ }
+ };
+ Call call = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .post(requestBody)
+ .build());
+ try {
+ call.execute();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("write body fail!", expected.getMessage());
+ }
+ }
+
private RecordedResponse executeSynchronously(Request request) throws IOException {
Response response = client.newCall(request).execute();
return new RecordedResponse(request, response, null, response.body().string(), null);
@@ -1852,11 +2166,15 @@
* -Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317}
*/
private void enableProtocol(Protocol protocol) {
+ enableTls();
+ client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
+ server.setProtocols(client.getProtocols());
+ }
+
+ private void enableTls() {
client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
- client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
server.useHttps(sslContext.getSocketFactory(), false);
- server.setProtocols(client.getProtocols());
}
private Buffer gzip(String data) throws IOException {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
index 91b5a59..7f40cd5 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
@@ -15,12 +15,10 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.SslContextBuilder;
import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
import java.util.Set;
import javax.net.ssl.SSLPeerUnverifiedException;
+import com.squareup.okhttp.internal.HeldCertificate;
import okio.ByteString;
import org.junit.Test;
@@ -32,39 +30,35 @@
import static org.junit.Assert.fail;
public final class CertificatePinnerTest {
- static SslContextBuilder sslContextBuilder;
+ static HeldCertificate certA1;
+ static String certA1Pin;
+ static ByteString certA1PinBase64;
- static KeyPair keyPairA;
- static X509Certificate keypairACertificate1;
- static String keypairACertificate1Pin;
- static ByteString keypairACertificate1PinBase64;
+ static HeldCertificate certB1;
+ static String certB1Pin;
+ static ByteString certB1PinBase64;
- static KeyPair keyPairB;
- static X509Certificate keypairBCertificate1;
- static String keypairBCertificate1Pin;
- static ByteString keypairBCertificate1PinBase64;
-
- static KeyPair keyPairC;
- static X509Certificate keypairCCertificate1;
- static String keypairCCertificate1Pin;
+ static HeldCertificate certC1;
+ static String certC1Pin;
static {
try {
- sslContextBuilder = new SslContextBuilder("example.com");
+ certA1 = new HeldCertificate.Builder()
+ .serialNumber("100")
+ .build();
+ certA1Pin = CertificatePinner.pin(certA1.certificate);
+ certA1PinBase64 = pinToBase64(certA1Pin);
- keyPairA = sslContextBuilder.generateKeyPair();
- keypairACertificate1 = sslContextBuilder.selfSignedCertificate(keyPairA, "1");
- keypairACertificate1Pin = CertificatePinner.pin(keypairACertificate1);
- keypairACertificate1PinBase64 = pinToBase64(keypairACertificate1Pin);
+ certB1 = new HeldCertificate.Builder()
+ .serialNumber("200")
+ .build();
+ certB1Pin = CertificatePinner.pin(certB1.certificate);
+ certB1PinBase64 = pinToBase64(certB1Pin);
- keyPairB = sslContextBuilder.generateKeyPair();
- keypairBCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairB, "1");
- keypairBCertificate1Pin = CertificatePinner.pin(keypairBCertificate1);
- keypairBCertificate1PinBase64 = pinToBase64(keypairBCertificate1Pin);
-
- keyPairC = sslContextBuilder.generateKeyPair();
- keypairCCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairC, "1");
- keypairCCertificate1Pin = CertificatePinner.pin(keypairCCertificate1);
+ certC1 = new HeldCertificate.Builder()
+ .serialNumber("300")
+ .build();
+ certC1Pin = CertificatePinner.pin(certC1.certificate);
} catch (GeneralSecurityException e) {
throw new AssertionError(e);
}
@@ -94,40 +88,46 @@
/** Multiple certificates generated from the same keypair have the same pin. */
@Test public void sameKeypairSamePin() throws Exception {
- X509Certificate keypairACertificate2 = sslContextBuilder.selfSignedCertificate(keyPairA, "2");
- String keypairACertificate2Pin = CertificatePinner.pin(keypairACertificate2);
+ HeldCertificate heldCertificateA2 = new HeldCertificate.Builder()
+ .keyPair(certA1.keyPair)
+ .serialNumber("101")
+ .build();
+ String keypairACertificate2Pin = CertificatePinner.pin(heldCertificateA2.certificate);
- X509Certificate keypairBCertificate2 = sslContextBuilder.selfSignedCertificate(keyPairB, "2");
- String keypairBCertificate2Pin = CertificatePinner.pin(keypairBCertificate2);
+ HeldCertificate heldCertificateB2 = new HeldCertificate.Builder()
+ .keyPair(certB1.keyPair)
+ .serialNumber("201")
+ .build();
+ String keypairBCertificate2Pin = CertificatePinner.pin(heldCertificateB2.certificate);
- assertTrue(keypairACertificate1Pin.equals(keypairACertificate2Pin));
- assertTrue(keypairBCertificate1Pin.equals(keypairBCertificate2Pin));
- assertFalse(keypairACertificate1Pin.equals(keypairBCertificate1Pin));
+ assertTrue(certA1Pin.equals(keypairACertificate2Pin));
+ assertTrue(certB1Pin.equals(keypairBCertificate2Pin));
+ assertFalse(certA1Pin.equals(certB1Pin));
}
@Test public void successfulCheck() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin)
+ .add("example.com", certA1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
}
@Test public void successfulMatchAcceptsAnyMatchingCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairBCertificate1Pin)
+ .add("example.com", certB1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1, keypairBCertificate1);
+ certificatePinner.check("example.com", certA1.certificate, certB1.certificate);
}
@Test public void unsuccessfulCheck() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin)
+ .add("example.com", certA1Pin)
.build();
try {
- certificatePinner.check("example.com", keypairBCertificate1);
+ certificatePinner.check("example.com", certB1.certificate);
fail();
} catch (SSLPeerUnverifiedException expected) {
}
@@ -135,51 +135,51 @@
@Test public void multipleCertificatesForOneHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+ .add("example.com", certA1Pin, certB1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1);
- certificatePinner.check("example.com", keypairBCertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
+ certificatePinner.check("example.com", certB1.certificate);
}
@Test public void multipleHostnamesForOneCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin)
- .add("www.example.com", keypairACertificate1Pin)
+ .add("example.com", certA1Pin)
+ .add("www.example.com", certA1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1);
- certificatePinner.check("www.example.com", keypairACertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
+ certificatePinner.check("www.example.com", certA1.certificate);
}
@Test public void absentHostnameMatches() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder().build();
- certificatePinner.check("example.com", keypairACertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
}
@Test public void successfulCheckForWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
+ .add("*.example.com", certA1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate);
}
@Test public void successfulMatchAcceptsAnyMatchingCertificateForWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairBCertificate1Pin)
+ .add("*.example.com", certB1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1, keypairBCertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate, certB1.certificate);
}
@Test public void unsuccessfulCheckForWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
+ .add("*.example.com", certA1Pin)
.build();
try {
- certificatePinner.check("a.example.com", keypairBCertificate1);
+ certificatePinner.check("a.example.com", certB1.certificate);
fail();
} catch (SSLPeerUnverifiedException expected) {
}
@@ -187,31 +187,31 @@
@Test public void multipleCertificatesForOneWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+ .add("*.example.com", certA1Pin, certB1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1);
- certificatePinner.check("a.example.com", keypairBCertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate);
+ certificatePinner.check("a.example.com", certB1.certificate);
}
@Test public void successfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
- .add("a.example.com", keypairBCertificate1Pin)
+ .add("*.example.com", certA1Pin)
+ .add("a.example.com", certB1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1);
- certificatePinner.check("a.example.com", keypairBCertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate);
+ certificatePinner.check("a.example.com", certB1.certificate);
}
@Test public void unsuccessfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
- .add("a.example.com", keypairBCertificate1Pin)
+ .add("*.example.com", certA1Pin)
+ .add("a.example.com", certB1Pin)
.build();
try {
- certificatePinner.check("a.example.com", keypairCCertificate1);
+ certificatePinner.check("a.example.com", certC1.certificate);
fail();
} catch (SSLPeerUnverifiedException expected) {
}
@@ -219,32 +219,32 @@
@Test public void successfulFindMatchingPins() {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("first.com", keypairACertificate1Pin, keypairBCertificate1Pin)
- .add("second.com", keypairCCertificate1Pin)
+ .add("first.com", certA1Pin, certB1Pin)
+ .add("second.com", certC1Pin)
.build();
- Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
- Set<ByteString> matchedPins = certificatePinner.findMatchingPins("first.com");
+ Set<ByteString> expectedPins = setOf(certA1PinBase64, certB1PinBase64);
+ Set<ByteString> matchedPins = certificatePinner.findMatchingPins("first.com");
assertEquals(expectedPins, matchedPins);
}
@Test public void successfulFindMatchingPinsForWildcardAndDirectCertificates() {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
- .add("a.example.com", keypairBCertificate1Pin)
- .add("b.example.com", keypairCCertificate1Pin)
+ .add("*.example.com", certA1Pin)
+ .add("a.example.com", certB1Pin)
+ .add("b.example.com", certC1Pin)
.build();
- Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
- Set<ByteString> matchedPins = certificatePinner.findMatchingPins("a.example.com");
+ Set<ByteString> expectedPins = setOf(certA1PinBase64, certB1PinBase64);
+ Set<ByteString> matchedPins = certificatePinner.findMatchingPins("a.example.com");
assertEquals(expectedPins, matchedPins);
}
@Test public void wildcardHostnameShouldNotMatchThroughDot() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
+ .add("*.example.com", certA1Pin)
.build();
assertNull(certificatePinner.findMatchingPins("example.com"));
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index d528c7a..e845eec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,570 +15,197 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
-import com.squareup.okhttp.internal.http.RecordingProxySelector;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.testing.RecordingHostnameVerifier;
-import java.io.IOException;
-import java.net.InetAddress;
+import com.squareup.okhttp.internal.RecordingOkAuthenticator;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Executor;
+import java.net.ProxySelector;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
import javax.net.SocketFactory;
-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.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
public final class ConnectionPoolTest {
- static {
- Internal.initializeInstanceForTests();
- }
-
- private static final List<ConnectionSpec> CONNECTION_SPECS = Util.immutableList(
- ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
-
- private static final int KEEP_ALIVE_DURATION_MS = 5000;
-
- private SSLContext sslContext = SslContextBuilder.localhost();
- private MockWebServer spdyServer;
- private InetSocketAddress spdySocketAddress;
- private Address spdyAddress;
-
- private MockWebServer httpServer;
- private Address httpAddress;
- private InetSocketAddress httpSocketAddress;
-
- private ConnectionPool pool;
- private FakeExecutor cleanupExecutor;
- private Connection httpA;
- private Connection httpB;
- private Connection httpC;
- private Connection httpD;
- private Connection httpE;
- private Connection spdyA;
-
- private Object owner;
-
- @Before public void setUp() throws Exception {
- setUp(2);
- }
-
- private void setUp(int poolSize) throws Exception {
- SocketFactory socketFactory = SocketFactory.getDefault();
- RecordingProxySelector proxySelector = new RecordingProxySelector();
-
- spdyServer = new MockWebServer();
- httpServer = new MockWebServer();
- spdyServer.useHttps(sslContext.getSocketFactory(), false);
-
- httpServer.start();
- httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
- null, null, AuthenticatorAdapter.INSTANCE, null,
- Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), CONNECTION_SPECS, proxySelector);
- httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
- httpServer.getPort());
-
- spdyServer.start();
- spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
- sslContext.getSocketFactory(), new RecordingHostnameVerifier(), CertificatePinner.DEFAULT,
- AuthenticatorAdapter.INSTANCE, null, Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1),
- CONNECTION_SPECS, proxySelector);
- spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
- spdyServer.getPort());
-
- Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress);
- Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress);
- pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS);
- // Disable the automatic execution of the cleanup.
- cleanupExecutor = new FakeExecutor();
- pool.replaceCleanupExecutorForTests(cleanupExecutor);
- httpA = new Connection(pool, httpRoute);
- httpA.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpB = new Connection(pool, httpRoute);
- httpB.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpC = new Connection(pool, httpRoute);
- httpC.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpD = new Connection(pool, httpRoute);
- httpD.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpE = new Connection(pool, httpRoute);
- httpE.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- spdyA = new Connection(pool, spdyRoute);
- spdyA.connect(20000, 20000, 2000, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-
- owner = new Object();
- httpA.setOwner(owner);
- httpB.setOwner(owner);
- httpC.setOwner(owner);
- httpD.setOwner(owner);
- httpE.setOwner(owner);
- }
-
- @After public void tearDown() throws Exception {
- httpServer.shutdown();
- spdyServer.shutdown();
-
- Util.closeQuietly(httpA.getSocket());
- Util.closeQuietly(httpB.getSocket());
- Util.closeQuietly(httpC.getSocket());
- Util.closeQuietly(httpD.getSocket());
- Util.closeQuietly(httpE.getSocket());
- Util.closeQuietly(spdyA.getSocket());
- }
-
- private void resetWithPoolSize(int poolSize) throws Exception {
- tearDown();
- setUp(poolSize);
- }
-
- @Test public void poolSingleHttpConnection() throws Exception {
- resetWithPoolSize(1);
- Connection connection = pool.get(httpAddress);
- assertNull(connection);
-
- connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress));
- connection.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- connection.setOwner(owner);
- assertEquals(0, pool.getConnectionCount());
-
- pool.recycle(connection);
- assertNull(connection.getOwner());
- assertEquals(1, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- Connection recycledConnection = pool.get(httpAddress);
- assertNull(connection.getOwner());
- assertEquals(connection, recycledConnection);
- assertTrue(recycledConnection.isAlive());
-
- recycledConnection = pool.get(httpAddress);
- assertNull(recycledConnection);
- }
-
- @Test public void getDoesNotScheduleCleanup() {
- Connection connection = pool.get(httpAddress);
- assertNull(connection);
- cleanupExecutor.assertExecutionScheduled(false);
- }
-
- @Test public void recycleSchedulesCleanup() {
- cleanupExecutor.assertExecutionScheduled(false);
- pool.recycle(httpA);
- cleanupExecutor.assertExecutionScheduled(true);
- }
-
- @Test public void shareSchedulesCleanup() {
- cleanupExecutor.assertExecutionScheduled(false);
- pool.share(spdyA);
- cleanupExecutor.assertExecutionScheduled(true);
- }
-
- @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
- pool.recycle(httpC);
- assertPooled(pool, httpC, httpB, httpA);
-
- pool.performCleanup();
- assertPooled(pool, httpC, httpB);
- }
-
- @Test public void getSpdyConnection() throws Exception {
- pool.share(spdyA);
- assertSame(spdyA, pool.get(spdyAddress));
- assertPooled(pool, spdyA);
- }
-
- @Test public void getHttpConnection() throws Exception {
- pool.recycle(httpA);
- assertSame(httpA, pool.get(httpAddress));
- assertPooled(pool);
- }
-
- @Test public void expiredConnectionNotReturned() throws Exception {
- pool.recycle(httpA);
-
- // Allow enough time to pass so that the connection is now expired.
- Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
-
- // The connection is held, but will not be returned.
- assertNull(pool.get(httpAddress));
- assertPooled(pool, httpA);
-
- // The connection must be cleaned up.
- pool.performCleanup();
- assertPooled(pool);
- }
-
- @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
- pool.recycle(httpC);
- pool.recycle(httpD);
- assertPooled(pool, httpD, httpC, httpB, httpA);
-
- pool.performCleanup();
- assertPooled(pool, httpD, httpC);
- }
-
- @Test public void expiredConnectionsAreEvicted() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
-
- // Allow enough time to pass so that the connections are now expired.
- Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
- assertPooled(pool, httpB, httpA);
-
- // The connections must be cleaned up.
- pool.performCleanup();
- assertPooled(pool);
- }
-
- @Test public void nonAliveConnectionNotReturned() throws Exception {
- pool.recycle(httpA);
-
- // Close the connection. It is an ex-connection. It has ceased to be.
- httpA.getSocket().close();
- assertPooled(pool, httpA);
- assertNull(pool.get(httpAddress));
-
- // The connection must be cleaned up.
- pool.performCleanup();
- assertPooled(pool);
- }
-
- @Test public void differentAddressConnectionNotReturned() throws Exception {
- pool.recycle(httpA);
- assertNull(pool.get(spdyAddress));
- assertPooled(pool, httpA);
- }
-
- @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
- pool.share(spdyA);
- pool.recycle(httpA);
- assertPooled(pool, httpA, spdyA);
- assertSame(spdyA, pool.get(spdyAddress));
- assertPooled(pool, spdyA, httpA);
- }
-
- @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
- assertSame(httpA, pool.get(httpAddress));
- }
-
- @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
- httpA.getSocket().shutdownInput();
- pool.recycle(httpA); // Should close httpA.
- assertTrue(httpA.getSocket().isClosed());
-
- // The pool should remain empty, and there is no need to schedule a cleanup.
- assertPooled(pool);
- cleanupExecutor.assertExecutionScheduled(false);
- }
-
- @Test public void shareHttpConnectionFails() throws Exception {
- try {
- pool.share(httpA);
- fail();
- } catch (IllegalArgumentException expected) {
+ private final Runnable emptyRunnable = new Runnable() {
+ @Override public void run() {
}
- // The pool should remain empty, and there is no need to schedule a cleanup.
- assertPooled(pool);
- cleanupExecutor.assertExecutionScheduled(false);
- }
+ };
- @Test public void recycleSpdyConnectionDoesNothing() throws Exception {
- pool.recycle(spdyA);
- // The pool should remain empty, and there is no need to schedule the cleanup.
- assertPooled(pool);
- cleanupExecutor.assertExecutionScheduled(false);
- }
+ private final Address addressA = newAddress("a");
+ private final Route routeA1 = newRoute(addressA);
+ private final Address addressB = newAddress("b");
+ private final Route routeB1 = newRoute(addressB);
+ private final Address addressC = newAddress("c");
+ private final Route routeC1 = newRoute(addressC);
- @Test public void validateIdleSpdyConnectionTimeout() throws Exception {
- pool.share(spdyA);
- assertPooled(pool, spdyA); // Connection should be in the pool.
+ @Test public void connectionsEvictedWhenIdleLongEnough() throws Exception {
+ ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
- pool.performCleanup();
- assertPooled(pool, spdyA); // Connection should still be in the pool.
+ RealConnection c1 = newConnection(pool, routeA1, 50L);
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
- pool.performCleanup();
- assertPooled(pool); // Connection should have been removed.
- }
+ // Running at time 50, the pool returns that nothing can be evicted until time 150.
+ assertEquals(100L, pool.cleanup(50L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- @Test public void validateIdleHttpConnectionTimeout() throws Exception {
- pool.recycle(httpA);
- assertPooled(pool, httpA); // Connection should be in the pool.
- cleanupExecutor.assertExecutionScheduled(true);
+ // Running at time 60, the pool returns that nothing can be evicted until time 150.
+ assertEquals(90L, pool.cleanup(60L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
- pool.performCleanup();
- assertPooled(pool, httpA); // Connection should still be in the pool.
+ // Running at time 149, the pool returns that nothing can be evicted until time 150.
+ assertEquals(1L, pool.cleanup(149L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
- pool.performCleanup();
- assertPooled(pool); // Connection should have been removed.
- }
-
- @Test public void maxConnections() throws IOException, InterruptedException {
- // Pool should be empty.
+ // Running at time 150, the pool evicts.
+ assertEquals(0, pool.cleanup(150L));
assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
- // http A should be added to the pool.
- pool.recycle(httpA);
- assertEquals(1, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- // http B should be added to the pool.
- pool.recycle(httpB);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- // http C should be added
- pool.recycle(httpC);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(3, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
-
- pool.performCleanup();
-
- // http A should be removed by cleanup.
- assertEquals(2, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- // spdy A should be added
- pool.share(spdyA);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
-
- pool.performCleanup();
-
- // http B should be removed by cleanup.
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // http C should be returned.
- Connection recycledHttpConnection = pool.get(httpAddress);
- recycledHttpConnection.setOwner(owner);
- assertNotNull(recycledHttpConnection);
- assertTrue(recycledHttpConnection.isAlive());
- assertEquals(1, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // spdy A will be returned but also 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.getMultiplexedConnectionCount());
-
- // http C should be added to the pool
- pool.recycle(httpC);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // 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.getMultiplexedConnectionCount());
-
- // spdy A will be returned but also kept in the pool.
- sharedSpdyConnection = pool.get(spdyAddress);
- assertEquals(spdyA, sharedSpdyConnection);
- assertNotNull(sharedSpdyConnection);
- assertEquals(1, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // http D should be added to the pool.
- pool.recycle(httpD);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // http E should be added to the pool.
- pool.recycle(httpE);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
-
- pool.performCleanup();
-
- // spdy A should be removed from the pool by cleanup.
- assertEquals(2, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
- }
-
- @Test public void connectionCleanup() throws Exception {
- ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
-
- // Add 3 connections to the pool.
- pool.recycle(httpA);
- pool.recycle(httpB);
- pool.share(spdyA);
-
- // Give the cleanup callable time to run and settle down.
- Thread.sleep(100);
-
- // Kill http A.
- Util.closeQuietly(httpA.getSocket());
-
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
-
- // Http A should be removed.
- pool.performCleanup();
- assertPooled(pool, spdyA, httpB);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // Now let enough time pass for the connections to expire.
- Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
-
- // All remaining connections should be removed.
- pool.performCleanup();
+ // Running again, the pool reports that no further runs are necessary.
+ assertEquals(-1, pool.cleanup(150L));
assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
}
- @Test public void maxIdleConnectionsLimitEnforced() throws Exception {
- ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
+ @Test public void inUseConnectionsNotEvicted() throws Exception {
+ ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- // Hit the max idle connections limit of 2.
- pool.recycle(httpA);
- pool.recycle(httpB);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpB, httpA);
+ RealConnection c1 = newConnection(pool, routeA1, 50L);
+ StreamAllocation streamAllocation = new StreamAllocation(pool, addressA);
+ streamAllocation.acquire(c1);
- // Adding httpC bumps httpA.
- pool.recycle(httpC);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpC, httpB);
+ // Running at time 50, the pool returns that nothing can be evicted until time 150.
+ assertEquals(100L, pool.cleanup(50L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- // Adding httpD bumps httpB.
- pool.recycle(httpD);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpD, httpC);
+ // Running at time 60, the pool returns that nothing can be evicted until time 160.
+ assertEquals(100L, pool.cleanup(60L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- // Adding httpE bumps httpC.
- pool.recycle(httpE);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpE, httpD);
+ // Running at time 160, the pool returns that nothing can be evicted until time 260.
+ assertEquals(100L, pool.cleanup(160L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
}
- @Test public void evictAllConnections() throws Exception {
- resetWithPoolSize(10);
- pool.recycle(httpA);
- Util.closeQuietly(httpA.getSocket()); // Include a closed connection in the pool.
- pool.recycle(httpB);
- pool.share(spdyA);
- int connectionCount = pool.getConnectionCount();
- assertTrue(connectionCount == 2 || connectionCount == 3);
+ @Test public void cleanupPrioritizesEarliestEviction() throws Exception {
+ ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- pool.evictAll();
+ RealConnection c1 = newConnection(pool, routeA1, 75L);
+ RealConnection c2 = newConnection(pool, routeB1, 50L);
+
+ // Running at time 75, the pool returns that nothing can be evicted until time 150.
+ assertEquals(75L, pool.cleanup(75L));
+ assertEquals(2, pool.getConnectionCount());
+
+ // Running at time 149, the pool returns that nothing can be evicted until time 150.
+ assertEquals(1L, pool.cleanup(149L));
+ assertEquals(2, pool.getConnectionCount());
+
+ // Running at time 150, the pool evicts c2.
+ assertEquals(0L, pool.cleanup(150L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
+ assertTrue(c2.socket.isClosed());
+
+ // Running at time 150, the pool returns that nothing can be evicted until time 175.
+ assertEquals(25L, pool.cleanup(150L));
+ assertEquals(1, pool.getConnectionCount());
+
+ // Running at time 175, the pool evicts c1.
+ assertEquals(0L, pool.cleanup(175L));
assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
+ assertTrue(c2.socket.isClosed());
}
- @Test public void closeIfOwnedBy() throws Exception {
- httpA.closeIfOwnedBy(owner);
- assertFalse(httpA.isAlive());
- assertFalse(httpA.clearOwner());
+ @Test public void oldestConnectionsEvictedIfIdleLimitExceeded() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
+
+ RealConnection c1 = newConnection(pool, routeA1, 50L);
+ RealConnection c2 = newConnection(pool, routeB1, 75L);
+
+ // With 2 connections, there's no need to evict until the connections time out.
+ assertEquals(50L, pool.cleanup(100L));
+ assertEquals(2, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
+ assertFalse(c2.socket.isClosed());
+
+ // Add a third connection
+ RealConnection c3 = newConnection(pool, routeC1, 75L);
+
+ // The third connection bounces the first.
+ assertEquals(0L, pool.cleanup(100L));
+ assertEquals(2, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
+ assertFalse(c2.socket.isClosed());
+ assertFalse(c3.socket.isClosed());
}
- @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception {
- httpA.closeIfOwnedBy(new Object());
- assertTrue(httpA.isAlive());
- assertTrue(httpA.clearOwner());
+ @Test public void leakedAllocation() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
+
+ RealConnection c1 = newConnection(pool, routeA1, 0L);
+ allocateAndLeakAllocation(pool, c1);
+
+ awaitGarbageCollection();
+ assertEquals(0L, pool.cleanup(100L));
+ assertEquals(Collections.emptyList(), c1.allocations);
+
+ assertTrue(c1.noNewStreams); // Can't allocate once a leak has been detected.
}
- @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception {
- try {
- spdyA.closeIfOwnedBy(owner);
- fail();
- } catch (IllegalStateException expected) {
- }
- }
-
- @Test public void cleanupRunnableStopsEventually() throws Exception {
- pool.recycle(httpA);
- pool.share(spdyA);
- assertPooled(pool, spdyA, httpA);
-
- // The cleanup should terminate once the pool is empty again.
- cleanupExecutor.fakeExecute();
- assertPooled(pool);
-
- cleanupExecutor.assertExecutionScheduled(false);
-
- // Adding a new connection should cause the cleanup to start up again.
- pool.recycle(httpB);
-
- cleanupExecutor.assertExecutionScheduled(true);
-
- // The cleanup should terminate once the pool is empty again.
- cleanupExecutor.fakeExecute();
- assertPooled(pool);
- }
-
- private void assertPooled(ConnectionPool pool, Connection... connections) throws Exception {
- assertEquals(Arrays.asList(connections), pool.getConnections());
+ /** Use a helper method so there's no hidden reference remaining on the stack. */
+ private void allocateAndLeakAllocation(ConnectionPool pool, RealConnection connection) {
+ StreamAllocation leak = new StreamAllocation(pool, connection.getRoute().getAddress());
+ leak.acquire(connection);
}
/**
- * An executor that does not actually execute anything by default. See
- * {@link #fakeExecute()}.
+ * See FinalizationTester for discussion on how to best trigger GC in tests.
+ * https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
+ * java/lang/ref/FinalizationTester.java
*/
- private static class FakeExecutor implements Executor {
+ private void awaitGarbageCollection() throws InterruptedException {
+ Runtime.getRuntime().gc();
+ Thread.sleep(100);
+ System.runFinalization();
+ }
- private Runnable runnable;
-
- @Override
- public void execute(Runnable runnable) {
- // This is a bonus assertion for the invariant: At no time should two runnables be scheduled.
- assertNull(this.runnable);
- this.runnable = runnable;
+ private RealConnection newConnection(ConnectionPool pool, Route route, long idleAtNanos) {
+ RealConnection connection = new RealConnection(route);
+ connection.idleAtNanos = idleAtNanos;
+ connection.socket = new Socket();
+ synchronized (pool) {
+ pool.put(connection);
}
+ return connection;
+ }
- public void assertExecutionScheduled(boolean expected) {
- assertEquals(expected, runnable != null);
- }
+ private Address newAddress(String name) {
+ return new Address(name, 1, Dns.SYSTEM, SocketFactory.getDefault(), null, null, null,
+ new RecordingOkAuthenticator("password"), null, Collections.<Protocol>emptyList(),
+ Collections.<ConnectionSpec>emptyList(),
+ ProxySelector.getDefault());
+ }
- /**
- * Executes the runnable.
- */
- public void fakeExecute() {
- Runnable toRun = this.runnable;
- this.runnable = null;
- toRun.run();
- }
+ private Route newRoute(Address address) {
+ return new Route(address, Proxy.NO_PROXY,
+ InetSocketAddress.createUnresolved(address.url().host(), address.url().port()));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java
new file mode 100644
index 0000000..f445dac
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2015 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.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import static org.junit.Assert.assertEquals;
+
+public final class ConnectionReuseTest {
+ @Rule public final TestRule timeout = new Timeout(30_000);
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private SSLContext sslContext = SslContextBuilder.localhost();
+ private OkHttpClient client = new OkHttpClient();
+
+ @Test public void connectionsAreReused() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionReused(request, request);
+ }
+
+ @Test public void connectionsAreReusedWithHttp2() throws Exception {
+ enableHttp2();
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionReused(request, request);
+ }
+
+ @Test public void connectionsAreNotReusedWithRequestConnectionClose() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request requestA = new Request.Builder()
+ .url(server.url("/"))
+ .header("Connection", "close")
+ .build();
+ Request requestB = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(requestA, requestB);
+ }
+
+ @Test public void connectionsAreNotReusedWithResponseConnectionClose() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Connection", "close")
+ .setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request requestA = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Request requestB = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(requestA, requestB);
+ }
+
+ @Test public void connectionsAreNotReusedWithUnknownLengthResponseBody() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("a")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)
+ .clearHeaders());
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(request, request);
+ }
+
+ @Test public void connectionsAreNotReusedIfPoolIsSizeZero() throws Exception {
+ client.setConnectionPool(new ConnectionPool(0, 5000));
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(request, request);
+ }
+
+ @Test public void connectionsReusedWithRedirectEvenIfPoolIsSizeZero() throws Exception {
+ client.setConnectionPool(new ConnectionPool(0, 5000));
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /b")
+ .setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("b", response.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void connectionsNotReusedWithRedirectIfDiscardingResponseIsSlow() throws Exception {
+ client.setConnectionPool(new ConnectionPool(0, 5000));
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /b")
+ .setBodyDelay(1, TimeUnit.SECONDS)
+ .setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("b", response.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void silentRetryWhenIdempotentRequestFailsOnReusedConnection() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+
+ Response responseA = client.newCall(request).execute();
+ assertEquals("a", responseA.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+
+ Response responseB = client.newCall(request).execute();
+ assertEquals("b", responseB.body().string());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void staleConnectionNotReusedForNonIdempotentRequest() throws Exception {
+ server.enqueue(new MockResponse().setBody("a")
+ .setSocketPolicy(SocketPolicy.SHUTDOWN_OUTPUT_AT_END));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request requestA = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response responseA = client.newCall(requestA).execute();
+ assertEquals("a", responseA.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+
+ Request requestB = new Request.Builder()
+ .url(server.url("/"))
+ .post(RequestBody.create(MediaType.parse("text/plain"), "b"))
+ .build();
+ Response responseB = client.newCall(requestB).execute();
+ assertEquals("b", responseB.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void http2ConnectionsAreSharedBeforeResponseIsConsumed() throws Exception {
+ enableHttp2();
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response response1 = client.newCall(request).execute();
+ Response response2 = client.newCall(request).execute();
+ response1.body().string(); // Discard the response body.
+ response2.body().string(); // Discard the response body.
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void connectionsAreEvicted() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ client.setConnectionPool(new ConnectionPool(5, 250, TimeUnit.MILLISECONDS));
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+
+ Response response1 = client.newCall(request).execute();
+ assertEquals("a", response1.body().string());
+
+ // Give the thread pool a chance to evict.
+ Thread.sleep(500);
+
+ Response response2 = client.newCall(request).execute();
+ assertEquals("b", response2.body().string());
+
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ private void enableHttp2() {
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.setProtocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1));
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.setProtocols(client.getProtocols());
+ }
+
+ private void assertConnectionReused(Request... requests) throws Exception {
+ for (int i = 0; i < requests.length; i++) {
+ Response response = client.newCall(requests[i]).execute();
+ response.body().string(); // Discard the response body.
+ assertEquals(i, server.takeRequest().getSequenceNumber());
+ }
+ }
+
+ private void assertConnectionNotReused(Request... requests) throws Exception {
+ for (Request request : requests) {
+ Response response = client.newCall(request).execute();
+ response.body().string(); // Discard the response body.
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
index 7833cca..3b390e8 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
@@ -15,29 +15,49 @@
*/
package com.squareup.okhttp;
-import org.junit.Test;
-
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
+import org.junit.Test;
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;
public final class ConnectionSpecTest {
+ @Test public void noTlsVersions() throws Exception {
+ try {
+ new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(new TlsVersion[0])
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {
+ assertEquals("At least one TLS version is required", expected.getMessage());
+ }
+ }
- @Test
- public void cleartextBuilder() throws Exception {
+ @Test public void noCipherSuites() throws Exception {
+ try {
+ new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .cipherSuites(new CipherSuite[0])
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {
+ assertEquals("At least one cipher suite is required", expected.getMessage());
+ }
+ }
+
+ @Test public void cleartextBuilder() throws Exception {
ConnectionSpec cleartextSpec = new ConnectionSpec.Builder(false).build();
assertFalse(cleartextSpec.isTls());
}
- @Test
- public void tlsBuilder_explicitCiphers() throws Exception {
+ @Test public void tlsBuilder_explicitCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -48,8 +68,7 @@
assertTrue(tlsSpec.supportsTlsExtensions());
}
- @Test
- public void tlsBuilder_defaultCiphers() throws Exception {
+ @Test public void tlsBuilder_defaultCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(true)
@@ -59,8 +78,7 @@
assertTrue(tlsSpec.supportsTlsExtensions());
}
- @Test
- public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
+ @Test public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(false)
@@ -79,17 +97,16 @@
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, false /* isFallback */);
- assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+ assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet =
- createSet(
+ set(
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
assertEquals(expectedCipherSet, expectedCipherSet);
}
- @Test
- public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
+ @Test public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(false)
@@ -108,10 +125,10 @@
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, true /* isFallback */);
- assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+ assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet =
- createSet(
+ set(
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
@@ -120,8 +137,7 @@
assertEquals(expectedCipherSet, expectedCipherSet);
}
- @Test
- public void tls_explicitCiphers() throws Exception {
+ @Test public void tls_explicitCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -141,17 +157,16 @@
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, true /* isFallback */);
- assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+ assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
- Set<String> expectedCipherSet = createSet(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
+ Set<String> expectedCipherSet = set(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
expectedCipherSet.add("TLS_FALLBACK_SCSV");
}
assertEquals(expectedCipherSet, expectedCipherSet);
}
- @Test
- public void tls_stringCiphersAndVersions() throws Exception {
+ @Test public void tls_stringCiphersAndVersions() throws Exception {
// Supporting arbitrary input strings allows users to enable suites and versions that are not
// yet known to the library, but are supported by the platform.
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
@@ -160,7 +175,7 @@
.build();
}
- public void tls_missingRequiredCipher() throws Exception {
+ @Test public void tls_missingRequiredCipher() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -185,8 +200,43 @@
assertFalse(tlsSpec.isCompatible(socket));
}
- @Test
- public void tls_missingTlsVersion() throws Exception {
+ @Test public void allEnabledCipherSuites() throws Exception {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledCipherSuites()
+ .build();
+ assertNull(tlsSpec.cipherSuites());
+
+ SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ sslSocket.setEnabledCipherSuites(new String[] {
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
+ });
+
+ tlsSpec.apply(sslSocket, false);
+ assertEquals(Arrays.asList(
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName),
+ Arrays.asList(sslSocket.getEnabledCipherSuites()));
+ }
+
+ @Test public void allEnabledTlsVersions() throws Exception {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledTlsVersions()
+ .build();
+ assertNull(tlsSpec.tlsVersions());
+
+ SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ sslSocket.setEnabledProtocols(new String[] {
+ TlsVersion.SSL_3_0.javaName(),
+ TlsVersion.TLS_1_1.javaName()
+ });
+
+ tlsSpec.apply(sslSocket, false);
+ assertEquals(Arrays.asList(TlsVersion.SSL_3_0.javaName(), TlsVersion.TLS_1_1.javaName()),
+ Arrays.asList(sslSocket.getEnabledProtocols()));
+ }
+
+ @Test public void tls_missingTlsVersion() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -206,7 +256,48 @@
assertFalse(tlsSpec.isCompatible(socket));
}
- private static Set<String> createSet(String... values) {
- return new LinkedHashSet<String>(Arrays.asList(values));
+ @Test public void equalsAndHashCode() throws Exception {
+ ConnectionSpec allCipherSuites = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledCipherSuites()
+ .build();
+ ConnectionSpec allTlsVersions = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledTlsVersions()
+ .build();
+
+ Set<Object> set = new CopyOnWriteArraySet<>();
+ assertTrue(set.add(ConnectionSpec.MODERN_TLS));
+ assertTrue(set.add(ConnectionSpec.COMPATIBLE_TLS));
+ assertTrue(set.add(ConnectionSpec.CLEARTEXT));
+ assertTrue(set.add(allTlsVersions));
+ assertTrue(set.add(allCipherSuites));
+
+ assertTrue(set.remove(ConnectionSpec.MODERN_TLS));
+ assertTrue(set.remove(ConnectionSpec.COMPATIBLE_TLS));
+ assertTrue(set.remove(ConnectionSpec.CLEARTEXT));
+ assertTrue(set.remove(allTlsVersions));
+ assertTrue(set.remove(allCipherSuites));
+ assertTrue(set.isEmpty());
+ }
+
+ @Test public void allEnabledToString() throws Exception {
+ ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledTlsVersions()
+ .allEnabledCipherSuites()
+ .build();
+ assertEquals("ConnectionSpec(cipherSuites=[all enabled], tlsVersions=[all enabled], "
+ + "supportsTlsExtensions=true)", connectionSpec.toString());
+ }
+
+ @Test public void simpleToString() throws Exception {
+ ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
+ .build();
+ assertEquals("ConnectionSpec(cipherSuites=[TLS_RSA_WITH_RC4_128_MD5], tlsVersions=[TLS_1_2], "
+ + "supportsTlsExtensions=true)", connectionSpec.toString());
+ }
+
+ private static <T> Set<T> set(T... values) {
+ return new LinkedHashSet<>(Arrays.asList(values));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
index 38a5de8..f72bd1a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
@@ -35,51 +35,43 @@
this.delegate = delegate;
}
- @Override
- public SSLSocket createSocket() throws IOException {
+ @Override public SSLSocket createSocket() throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket();
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+ @Override public SSLSocket createSocket(String host, int port) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(String host, int port, InetAddress localAddress, int localPort)
- throws IOException, UnknownHostException {
+ @Override public SSLSocket createSocket(
+ String host, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+ @Override public SSLSocket createSocket(InetAddress host, int port) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort)
- throws IOException {
+ @Override public SSLSocket createSocket(
+ InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
return configureSocket(sslSocket);
}
- @Override
- public String[] getDefaultCipherSuites() {
+ @Override public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}
- @Override
- public String[] getSupportedCipherSuites() {
+ @Override public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
- @Override
- public SSLSocket createSocket(Socket socket, String host, int port, boolean autoClose)
- throws IOException {
+ @Override public SSLSocket createSocket(
+ Socket socket, String host, int port, boolean autoClose) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(socket, host, port, autoClose);
return configureSocket(sslSocket);
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
index d45ac10..466126d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
@@ -692,6 +692,8 @@
HttpUrl.parse("http://host/%/b").pathSegments());
assertEquals(Arrays.asList("%"),
HttpUrl.parse("http://host/%").pathSegments());
+ assertEquals(Arrays.asList("%00"),
+ HttpUrl.parse("http://github.com/%%30%30").pathSegments());
}
@Test public void malformedUtf8Encoding() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
index d8454ac..e70c908 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
@@ -135,7 +135,7 @@
Interceptor interceptor = new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
Address address = chain.connection().getRoute().getAddress();
- String sameHost = address.getRfc2732Host();
+ String sameHost = address.getUriHost();
int differentPort = address.getUriPort() + 1;
return chain.proceed(chain.request().newBuilder()
.url(HttpUrl.parse("http://" + sameHost + ":" + differentPort + "/"))
@@ -210,6 +210,35 @@
assertEquals("abcabcabc", response.body().string());
}
+ @Test public void networkInterceptorsCanChangeRequestMethodFromGetToPost() throws Exception {
+ server.enqueue(new MockResponse());
+
+ client.networkInterceptors().add(new Interceptor() {
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ MediaType mediaType = MediaType.parse("text/plain");
+ RequestBody body = RequestBody.create(mediaType, "abc");
+ return chain.proceed(originalRequest.newBuilder()
+ .method("POST", body)
+ .header("Content-Type", mediaType.toString())
+ .header("Content-Length", Long.toString(body.contentLength()))
+ .build());
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .get()
+ .build();
+
+ client.newCall(request).execute();
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals("abc", recordedRequest.getBody().readUtf8());
+ }
+
@Test public void applicationInterceptorsRewriteRequestToServer() throws Exception {
rewriteRequestToServer(client.interceptors());
}
@@ -362,7 +391,8 @@
client.interceptors().add(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
- chain.proceed(chain.request());
+ Response response1 = chain.proceed(chain.request());
+ response1.body().close();
return chain.proceed(chain.request());
}
});
@@ -468,14 +498,6 @@
}
}
- @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
- interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
- }
-
- @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
- interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
- }
-
@Test public void networkInterceptorModifiedRequestIsReturned() throws IOException {
server.enqueue(new MockResponse());
@@ -500,6 +522,14 @@
assertEquals("intercepted request", response.networkResponse().request().header("User-Agent"));
}
+ @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
+ interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
+ }
+
+ @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
+ interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
+ }
+
/**
* When an interceptor throws an unexpected exception, asynchronous callers are left hanging. The
* exception goes to the uncaught exception handler.
@@ -525,6 +555,57 @@
assertEquals("boom!", executor.takeException().getMessage());
}
+ @Test public void applicationInterceptorReturnsNull() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ chain.proceed(chain.request());
+ return null;
+ }
+ };
+ client.interceptors().add(interceptor);
+
+ ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
+ client.setDispatcher(new Dispatcher(executor));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (NullPointerException expected) {
+ assertEquals("application interceptor " + interceptor
+ + " returned null", expected.getMessage());
+ }
+ }
+
+ @Test public void networkInterceptorReturnsNull() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ chain.proceed(chain.request());
+ return null;
+ }
+ };
+ client.networkInterceptors().add(interceptor);
+
+ ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
+ client.setDispatcher(new Dispatcher(executor));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (NullPointerException expected) {
+ assertEquals("network interceptor " + interceptor + " returned null", expected.getMessage());
+ }
+ }
+
private RequestBody uppercase(final RequestBody original) {
return new RequestBody() {
@Override public MediaType contentType() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
index 9d65147..f2447ec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
@@ -20,7 +20,6 @@
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import okio.Buffer;
/**
* Records received HTTP responses so they can be later retrieved by tests.
@@ -36,11 +35,8 @@
}
@Override public synchronized void onResponse(Response response) throws IOException {
- Buffer buffer = new Buffer();
- ResponseBody body = response.body();
- body.source().readAll(buffer);
-
- responses.add(new RecordedResponse(response.request(), response, null, buffer.readUtf8(), null));
+ String body = response.body().string();
+ responses.add(new RecordedResponse(response.request(), response, null, body, null));
notifyAll();
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
index e2a5532..651ea8b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
@@ -41,6 +41,8 @@
* See <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928</a>.
*/
public final class SocksProxy {
+ public final String HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS = "onlyProxyCanResolveMe.org";
+
private static final int VERSION_5 = 5;
private static final int METHOD_NONE = 0xff;
private static final int METHOD_NO_AUTHENTICATION_REQUIRED = 0;
@@ -156,7 +158,10 @@
case ADDRESS_TYPE_DOMAIN_NAME:
int domainNameLength = fromSource.readByte() & 0xff;
String domainName = fromSource.readUtf8(domainNameLength);
- toAddress = InetAddress.getByName(domainName);
+ // Resolve HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS to localhost.
+ toAddress = domainName.equalsIgnoreCase(HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
+ ? InetAddress.getByName("localhost")
+ : InetAddress.getByName(domainName);
break;
default:
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
index 377ff83..0d6e9fd 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
@@ -85,4 +85,23 @@
assertEquals(1, socksProxy.connectionCount());
}
+
+ @Test public void checkRemoteDNSResolve() throws Exception {
+ // This testcase will fail if the target is resolved locally instead of through the proxy.
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ OkHttpClient client = new OkHttpClient()
+ .setProxy(socksProxy.proxy());
+
+ HttpUrl url = server.url("/")
+ .newBuilder()
+ .host(socksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
+ .build();
+
+ Request request = new Request.Builder().url(url).build();
+ Response response1 = client.newCall(request).execute();
+ assertEquals("abc", response1.body().string());
+
+ assertEquals(1, socksProxy.connectionCount());
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
index 7c948b8..f021249 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
@@ -17,9 +17,10 @@
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.RecordingAuthenticator;
import com.squareup.okhttp.internal.RecordingOkAuthenticator;
-import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SingleInetAddressDns;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.Version;
@@ -47,6 +48,7 @@
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
+import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@@ -226,6 +228,7 @@
assertEquals("d", connection.getHeaderField(1));
assertEquals("A", connection.getHeaderFieldKey(2));
assertEquals("e", connection.getHeaderField(2));
+ connection.getInputStream().close();
}
@Test public void serverSendsInvalidResponseHeaders() throws Exception {
@@ -318,6 +321,7 @@
server.enqueue(new MockResponse().setBody("A"));
connection = client.open(server.getUrl("/"));
assertNull(connection.getErrorStream());
+ connection.getInputStream().close();
}
@Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
@@ -335,8 +339,13 @@
server.enqueue(response);
server.enqueue(response);
- assertContent("ABCDE", client.open(server.getUrl("/")), 5);
- assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+ HttpURLConnection c1 = client.open(server.getUrl("/"));
+ assertContent("ABCDE", c1, 5);
+ HttpURLConnection c2 = client.open(server.getUrl("/"));
+ assertContent("ABCDE", c2, 5);
+
+ c1.getInputStream().close();
+ c2.getInputStream().close();
}
// Check that we recognize a few basic mime types by extension.
@@ -607,7 +616,7 @@
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
suppressTlsFallbackScsv(client.client());
- Internal.instance.setNetwork(client.client(), new SingleInetAddressNetwork());
+ client.client().setDns(new SingleInetAddressDns());
client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/foo"));
@@ -883,7 +892,7 @@
// Configure a single IP address for the host and a single configuration, so we only need one
// failure to fail permanently.
- Internal.instance.setNetwork(client.client(), new SingleInetAddressNetwork());
+ client.client().setDns(new SingleInetAddressDns());
client.client().setSslSocketFactory(sslContext.getSocketFactory());
client.client().setConnectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS));
client.client().setHostnameVerifier(new RecordingHostnameVerifier());
@@ -926,8 +935,8 @@
RecordedRequest connect = server.takeRequest();
assertNull(connect.getHeader("Private"));
- assertEquals("bar", connect.getHeader("Proxy-Authorization"));
- assertEquals("baz", connect.getHeader("User-Agent"));
+ assertNull(connect.getHeader("Proxy-Authorization"));
+ assertEquals(Version.userAgent(), connect.getHeader("User-Agent"));
assertEquals("android.com", connect.getHeader("Host"));
assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
@@ -1023,6 +1032,7 @@
fail("Expected a connection closed exception");
} catch (IOException expected) {
}
+ in.close();
}
@Test public void disconnectBeforeConnect() throws IOException {
@@ -1085,6 +1095,7 @@
} catch (IOException expected) {
}
assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
+ in.close();
assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/")));
}
@@ -1108,6 +1119,7 @@
assertEquals(401, conn.getResponseCode());
assertEquals(401, conn.getResponseCode());
assertEquals(1, server.getRequestCount());
+ conn.getErrorStream().close();
}
@Test public void nonHexChunkSize() throws IOException {
@@ -1121,6 +1133,7 @@
fail();
} catch (IOException e) {
}
+ connection.getInputStream().close();
}
@Test public void malformedChunkSize() throws IOException {
@@ -1133,6 +1146,8 @@
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail();
} catch (IOException e) {
+ } finally {
+ connection.getInputStream().close();
}
}
@@ -1156,6 +1171,8 @@
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail();
} catch (IOException e) {
+ } finally {
+ connection.getInputStream().close();
}
}
@@ -1286,7 +1303,7 @@
HttpURLConnection connection = client.open(server.getUrl("/"));
assertContent("{}", connection);
- assertEquals(0, client.client().getConnectionPool().getConnectionCount());
+ assertEquals(0, client.client().getConnectionPool().getIdleConnectionCount());
}
@Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
@@ -1358,6 +1375,7 @@
OutputStream outputStream = connection.getOutputStream();
outputStream.write(body.getBytes("US-ASCII"));
assertEquals(200, connection.getResponseCode());
+ connection.getInputStream().close();
RecordedRequest request = server.takeRequest();
assertEquals(body, request.getBody().readUtf8());
@@ -1511,7 +1529,8 @@
int responseCode = proxy ? 407 : 401;
RecordingAuthenticator authenticator = new RecordingAuthenticator(null);
Authenticator.setDefault(authenticator);
- MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(responseCode)
+ MockResponse pleaseAuthenticate = new MockResponse()
+ .setResponseCode(responseCode)
.addHeader(authHeader)
.setBody("Please authenticate.");
server.enqueue(pleaseAuthenticate);
@@ -1523,6 +1542,7 @@
connection = client.open(server.getUrl("/"));
}
assertEquals(responseCode, connection.getResponseCode());
+ connection.getErrorStream().close();
return authenticator.calls;
}
@@ -1968,7 +1988,7 @@
assertContent("This is the 2nd server!", client.open(server.getUrl("/a")));
- assertEquals(Arrays.asList(server.getUrl("/a").toURI(), server2.getUrl("/b").toURI()),
+ assertEquals(Arrays.asList(server.getUrl("/").toURI(), server2.getUrl("/").toURI()),
proxySelectionRequests);
}
@@ -2190,9 +2210,9 @@
@Test public void httpsWithCustomTrustManager() throws Exception {
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- RecordingTrustManager trustManager = new RecordingTrustManager();
+ RecordingTrustManager trustManager = new RecordingTrustManager(sslContext);
SSLContext sc = SSLContext.getInstance("TLS");
- sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
+ sc.init(null, new TrustManager[] {trustManager}, new SecureRandom());
client.client().setHostnameVerifier(hostnameVerifier);
client.client().setSslSocketFactory(sc.getSocketFactory());
@@ -2206,8 +2226,7 @@
assertContent("DEF", client.open(url));
assertContent("GHI", client.open(url));
- assertEquals(Arrays.asList("verify " + server.getHostName()),
- hostnameVerifier.calls);
+ assertEquals(Arrays.asList("verify " + server.getHostName()), hostnameVerifier.calls);
assertEquals(Arrays.asList("checkServerTrusted [CN=" + server.getHostName() + " 1]"),
trustManager.calls);
}
@@ -2232,6 +2251,7 @@
fail();
} catch (SocketTimeoutException expected) {
}
+ in.close();
}
/** Confirm that an unacknowledged write times out. */
@@ -2500,6 +2520,7 @@
} catch (NullPointerException expected) {
}
assertNull(connection.getContent(new Class[] { getClass() }));
+ connection.getInputStream().close();
}
@Test public void getOutputStreamOnGetFails() throws Exception {
@@ -2510,6 +2531,7 @@
fail();
} catch (ProtocolException expected) {
}
+ connection.getInputStream().close();
}
@Test public void getOutputAfterGetInputStreamFails() throws Exception {
@@ -2538,6 +2560,7 @@
fail();
} catch (IllegalStateException expected) {
}
+ connection.getInputStream().close();
}
@Test public void clientSendsContentLength() throws Exception {
@@ -2550,24 +2573,28 @@
assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
assertEquals("3", request.getHeader("Content-Length"));
+ connection.getInputStream().close();
}
@Test public void getContentLengthConnects() throws Exception {
server.enqueue(new MockResponse().setBody("ABC"));
connection = client.open(server.getUrl("/"));
assertEquals(3, connection.getContentLength());
+ connection.getInputStream().close();
}
@Test public void getContentTypeConnects() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("ABC"));
connection = client.open(server.getUrl("/"));
assertEquals("text/plain", connection.getContentType());
+ connection.getInputStream().close();
}
@Test public void getContentEncodingConnects() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Encoding: identity").setBody("ABC"));
connection = client.open(server.getUrl("/"));
assertEquals("identity", connection.getContentEncoding());
+ connection.getInputStream().close();
}
// http://b/4361656
@@ -2797,6 +2824,7 @@
connection = client.open(server.getUrl("/"));
connection.getResponseCode();
assertEquals("A", connection.getHeaderField(""));
+ connection.getInputStream().close();
}
@Test public void requestHeaderValidationIsStrict() throws Exception {
@@ -3380,9 +3408,14 @@
private static class RecordingTrustManager implements X509TrustManager {
private final List<String> calls = new ArrayList<String>();
+ private final X509TrustManager delegate;
+
+ public RecordingTrustManager(SSLContext sslContext) {
+ this.delegate = Platform.get().trustManager(sslContext.getSocketFactory());
+ }
public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[] { };
+ return delegate.getAcceptedIssuers();
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
similarity index 71%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
index 4934b42..759ac5d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
@@ -15,16 +15,19 @@
*/
package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Dns;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
/**
* A network that always resolves two IP addresses per host. Use this when testing route selection
* fallbacks to guarantee that a fallback address is available.
*/
-public class DoubleInetAddressNetwork implements Network {
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- InetAddress[] allInetAddresses = Network.DEFAULT.resolveInetAddresses(host);
- return new InetAddress[] { allInetAddresses[0], allInetAddresses[0] };
+public class DoubleInetAddressDns implements Dns {
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
+ return Arrays.asList(addresses.get(0), addresses.get(0));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
similarity index 72%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
index beb48cb..43cbe63 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
@@ -15,17 +15,20 @@
*/
package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Dns;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.List;
/**
* A network that resolves only one IP address per host. Use this when testing
* route selection fallbacks to prevent the host machine's various IP addresses
* from interfering.
*/
-public class SingleInetAddressNetwork implements Network {
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- InetAddress[] allInetAddresses = Network.DEFAULT.resolveInetAddresses(host);
- return new InetAddress[] { allInetAddresses[0] };
+public class SingleInetAddressDns implements Dns {
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
+ return Collections.singletonList(addresses.get(0));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
index 24c512d..1e00b4b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
@@ -443,7 +443,8 @@
String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
Socket socket = peer.openSocket();
- FramedConnection connection = new FramedConnection.Builder(true, socket)
+ FramedConnection connection = new FramedConnection.Builder(true)
+ .socket(socket)
.pushObserver(IGNORE)
.protocol(HTTP_2.getProtocol())
.build();
@@ -488,7 +489,8 @@
private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
throws IOException {
- return new FramedConnection.Builder(true, peer.openSocket())
+ return new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
.pushObserver(IGNORE)
.protocol(variant.getProtocol());
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
similarity index 65%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
index 91ba56c..7947c03 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.PushPromise;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import java.net.HttpURLConnection;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -78,4 +79,36 @@
assertEquals("HEAD /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
assertEquals("bar", pushedRequest.getHeader("foo"));
}
+
+ /**
+ * Push a setting that permits up to 2 concurrent streams, then make 3 concurrent requests and
+ * confirm that the third concurrent request prepared a new connection.
+ */
+ @Test public void settingsLimitsMaxConcurrentStreams() throws Exception {
+ Settings settings = new Settings();
+ settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 2);
+
+ // Read & write a full request to confirm settings are accepted.
+ server.enqueue(new MockResponse().withSettings(settings));
+ HttpURLConnection settingsConnection = client.open(server.getUrl("/"));
+ assertContent("", settingsConnection, Integer.MAX_VALUE);
+
+ server.enqueue(new MockResponse().setBody("ABC"));
+ server.enqueue(new MockResponse().setBody("DEF"));
+ server.enqueue(new MockResponse().setBody("GHI"));
+
+ HttpURLConnection connection1 = client.open(server.getUrl("/"));
+ connection1.connect();
+ HttpURLConnection connection2 = client.open(server.getUrl("/"));
+ connection2.connect();
+ HttpURLConnection connection3 = client.open(server.getUrl("/"));
+ connection3.connect();
+ assertContent("ABC", connection1, Integer.MAX_VALUE);
+ assertContent("DEF", connection2, Integer.MAX_VALUE);
+ assertContent("GHI", connection3, Integer.MAX_VALUE);
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // Settings connection.
+ assertEquals(1, server.takeRequest().getSequenceNumber()); // Reuse settings connection.
+ assertEquals(2, server.takeRequest().getSequenceNumber()); // Reuse settings connection.
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection!
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
similarity index 94%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
index 4020bf4..5e9bf61 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.Protocol;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
similarity index 99%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
index 2d52eee..1f7d999 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.Cache;
import com.squareup.okhttp.ConnectionPool;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
index 26d4986..752e92b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
@@ -150,15 +150,18 @@
// play it back
final AtomicInteger receiveCount = new AtomicInteger();
- IncomingStreamHandler handler = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
+ FramedConnection.Listener handler = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
receiveCount.incrementAndGet();
assertEquals(pushHeaders, stream.getRequestHeaders());
assertEquals(null, stream.getErrorCode());
stream.reply(headerEntries("b", "banana"), true);
}
};
- new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
+ new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
+ .listener(handler)
+ .build();
// verify the peer received what was expected
MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -178,14 +181,14 @@
// play it back
final AtomicInteger receiveCount = new AtomicInteger();
- IncomingStreamHandler handler = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
+ FramedConnection.Listener listener = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
stream.reply(headerEntries("b", "banana"), false);
receiveCount.incrementAndGet();
}
};
- connectionBuilder(peer, SPDY3).handler(handler).build();
+ connectionBuilder(peer, SPDY3).listener(listener).build();
// verify the peer received what was expected
MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -254,7 +257,7 @@
@Test public void serverSendsSettingsToClient() throws Exception {
// write the mocking script
- Settings settings = new Settings();
+ final Settings settings = new Settings();
settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10);
peer.sendFrame().settings(settings);
peer.sendFrame().ping(false, 2, 0);
@@ -262,12 +265,24 @@
peer.play();
// play it back
- FramedConnection connection = connection(peer, SPDY3);
+ final AtomicInteger maxConcurrentStreams = new AtomicInteger();
+ FramedConnection.Listener listener = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
+ throw new AssertionError();
+ }
+ @Override public void onSettings(FramedConnection connection) {
+ maxConcurrentStreams.set(connection.maxConcurrentStreams());
+ }
+ };
+ FramedConnection connection = connectionBuilder(peer, SPDY3)
+ .listener(listener)
+ .build();
peer.takeFrame(); // Guarantees that the peer Settings frame has been processed.
synchronized (connection) {
assertEquals(10, connection.peerSettings.getMaxConcurrentStreams(-1));
}
+ assertEquals(10, maxConcurrentStreams.get());
}
@Test public void multipleSettingsFramesAreMerged() throws Exception {
@@ -613,15 +628,18 @@
// play it back
final AtomicInteger receiveCount = new AtomicInteger();
- IncomingStreamHandler handler = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
+ FramedConnection.Listener listener = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
receiveCount.incrementAndGet();
assertEquals(headerEntries("a", "android"), stream.getRequestHeaders());
assertEquals(null, stream.getErrorCode());
stream.reply(headerEntries("c", "cola"), true);
}
};
- new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
+ new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
+ .listener(listener)
+ .build();
// verify the peer received what was expected
MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -1315,7 +1333,8 @@
String longString = ByteString.of(randomBytes(2048)).base64();
Socket socket = peer.openSocket();
- FramedConnection connection = new FramedConnection.Builder(true, socket)
+ FramedConnection connection = new FramedConnection.Builder(true)
+ .socket(socket)
.protocol(SPDY3.getProtocol())
.build();
socket.shutdownOutput();
@@ -1343,7 +1362,8 @@
private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
throws IOException {
- return new FramedConnection.Builder(true, peer.openSocket())
+ return new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
.protocol(variant.getProtocol());
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java
new file mode 100644
index 0000000..03d4347
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Dns;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public final class FakeDns implements Dns {
+ private List<String> requestedHosts = new ArrayList<>();
+ private List<InetAddress> addresses = Collections.emptyList();
+
+ /** Sets the addresses to be returned by this fake DNS service. */
+ public FakeDns addresses(List<InetAddress> addresses) {
+ this.addresses = new ArrayList<>(addresses);
+ return this;
+ }
+
+ /** Sets the service to throw when a hostname is requested. */
+ public FakeDns unknownHost() {
+ this.addresses = Collections.emptyList();
+ return this;
+ }
+
+ public InetAddress address(int index) {
+ return addresses.get(index);
+ }
+
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ requestedHosts.add(hostname);
+ if (addresses.isEmpty()) throw new UnknownHostException();
+ return addresses;
+ }
+
+ public void assertRequests(String... expectedHosts) {
+ assertEquals(Arrays.asList(expectedHosts), requestedHosts);
+ requestedHosts.clear();
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
index 1f5ad6d..ea6e024 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
@@ -26,7 +26,6 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-
import org.junit.Test;
import static com.squareup.okhttp.TestUtil.headerEntries;
@@ -42,24 +41,20 @@
":status", "200 OK",
":version", "HTTP/1.1");
Request request = new Request.Builder().url("http://square.com/").build();
- Response response =
- FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+ Response response = Http2xStream.readSpdy3HeadersList(headerBlock).request(request).build();
Headers headers = response.headers();
- assertEquals(4, headers.size());
+ assertEquals(3, headers.size());
assertEquals(Protocol.SPDY_3, response.protocol());
assertEquals(200, response.code());
assertEquals("OK", response.message());
assertEquals("no-cache, no-store", headers.get("cache-control"));
assertEquals("Cookie2", headers.get("set-cookie"));
- assertEquals(Protocol.SPDY_3.toString(), headers.get(OkHeaders.SELECTED_PROTOCOL));
- assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
- assertEquals("cache-control", headers.name(1));
- assertEquals("no-cache, no-store", headers.value(1));
+ assertEquals("cache-control", headers.name(0));
+ assertEquals("no-cache, no-store", headers.value(0));
+ assertEquals("set-cookie", headers.name(1));
+ assertEquals("Cookie1", headers.value(1));
assertEquals("set-cookie", headers.name(2));
- assertEquals("Cookie1", headers.value(2));
- assertEquals("set-cookie", headers.name(3));
- assertEquals("Cookie2", headers.value(3));
+ assertEquals("Cookie2", headers.value(2));
assertNull(headers.get(":status"));
assertNull(headers.get(":version"));
}
@@ -70,12 +65,9 @@
":version", "HTTP/1.1",
"connection", "close");
Request request = new Request.Builder().url("http://square.com/").build();
- Response response =
- FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+ Response response = Http2xStream.readSpdy3HeadersList(headerBlock).request(request).build();
Headers headers = response.headers();
- assertEquals(1, headers.size());
- assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
+ assertEquals(0, headers.size());
}
@Test public void readNameValueBlockDropsForbiddenHeadersHttp2() throws IOException {
@@ -84,15 +76,14 @@
":version", "HTTP/1.1",
"connection", "close");
Request request = new Request.Builder().url("http://square.com/").build();
- Response response = FramedTransport.readNameValueBlock(headerBlock, Protocol.HTTP_2)
- .request(request).build();
+ Response response = Http2xStream.readHttp2HeadersList(headerBlock).request(request).build();
Headers headers = response.headers();
assertEquals(1, headers.size());
- assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.HTTP_2.toString(), headers.value(0));
+ assertEquals(":version", headers.name(0));
+ assertEquals("HTTP/1.1", headers.value(0));
}
- @Test public void toNameValueBlock() {
+ @Test public void spdy3HeadersList() {
Request request = new Request.Builder()
.url("http://square.com/")
.header("cache-control", "no-cache, no-store")
@@ -100,8 +91,7 @@
.addHeader("set-cookie", "Cookie2")
.header(":status", "200 OK")
.build();
- List<Header> headerBlock =
- FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1");
+ List<Header> headerBlock = Http2xStream.spdy3HeadersList(request);
List<Header> expected = headerEntries(
":method", "GET",
":path", "/",
@@ -114,7 +104,7 @@
assertEquals(expected, headerBlock);
}
- @Test public void toNameValueBlockDropsForbiddenHeadersSpdy3() {
+ @Test public void spdy3HeadersListDropsForbiddenHeadersSpdy3() {
Request request = new Request.Builder()
.url("http://square.com/")
.header("Connection", "close")
@@ -126,10 +116,10 @@
":version", "HTTP/1.1",
":host", "square.com",
":scheme", "http");
- assertEquals(expected, FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1"));
+ assertEquals(expected, Http2xStream.spdy3HeadersList(request));
}
- @Test public void toNameValueBlockDropsForbiddenHeadersHttp2() {
+ @Test public void http2HeadersListDropsForbiddenHeadersHttp2() {
Request request = new Request.Builder()
.url("http://square.com/")
.header("Connection", "upgrade")
@@ -140,8 +130,7 @@
":path", "/",
":authority", "square.com",
":scheme", "http");
- assertEquals(expected,
- FramedTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
+ assertEquals(expected, Http2xStream.http2HeadersList(request));
}
@Test public void ofTrims() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index bb8d082..8560e0f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -17,14 +17,9 @@
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Authenticator;
-import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.ConnectionSpec;
-import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.Network;
import com.squareup.okhttp.internal.RouteDatabase;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
@@ -57,14 +52,14 @@
ConnectionSpec.CLEARTEXT);
private static final int proxyAPort = 1001;
- private static final String proxyAHost = "proxyA";
+ private static final String proxyAHost = "proxya";
private static final Proxy proxyA =
- new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort));
+ new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyAHost, proxyAPort));
private static final int proxyBPort = 1002;
- private static final String proxyBHost = "proxyB";
+ private static final String proxyBHost = "proxyb";
private static final Proxy proxyB =
- new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort));
- private String uriHost = "hostA";
+ new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyBHost, proxyBPort));
+ private String uriHost = "hosta";
private int uriPort = 1003;
private SocketFactory socketFactory;
@@ -76,43 +71,20 @@
private final List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1);
private final FakeDns dns = new FakeDns();
private final RecordingProxySelector proxySelector = new RecordingProxySelector();
- private OkHttpClient client;
- private RouteDatabase routeDatabase;
- private Request httpRequest;
- private Request httpsRequest;
+ private RouteDatabase routeDatabase = new RouteDatabase();
@Before public void setUp() throws Exception {
socketFactory = SocketFactory.getDefault();
hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
-
- client = new OkHttpClient()
- .setAuthenticator(authenticator)
- .setProxySelector(proxySelector)
- .setSocketFactory(socketFactory)
- .setSslSocketFactory(sslSocketFactory)
- .setHostnameVerifier(hostnameVerifier)
- .setProtocols(protocols)
- .setConnectionSpecs(connectionSpecs)
- .setConnectionPool(ConnectionPool.getDefault());
- Internal.instance.setNetwork(client, dns);
-
- routeDatabase = Internal.instance.routeDatabase(client);
-
- httpRequest = new Request.Builder()
- .url("http://" + uriHost + ":" + uriPort + "/path")
- .build();
- httpsRequest = new Request.Builder()
- .url("https://" + uriHost + ":" + uriPort + "/path")
- .build();
}
@Test public void singleRoute() throws Exception {
Address address = httpAddress();
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -125,14 +97,14 @@
@Test public void singleRouteReturnsFailedRoute() throws Exception {
Address address = httpAddress();
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
+ dns.addresses(makeFakeAddresses(255, 1));
Route route = routeSelector.next();
routeDatabase.failed(route);
- routeSelector = RouteSelector.get(address, httpRequest, client);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ routeSelector = new RouteSelector(address, routeDatabase);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
assertFalse(routeSelector.hasNext());
try {
routeSelector.next();
@@ -142,15 +114,14 @@
}
@Test public void explicitProxyTriesThatProxysAddressesOnly() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator,
- proxyA, protocols, connectionSpecs, proxySelector);
- client.setProxy(proxyA);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ Address address = new Address(uriHost, uriPort, dns, socketFactory, null, null, null,
+ authenticator, proxyA, protocols, connectionSpecs, proxySelector);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(proxyAHost);
@@ -158,15 +129,14 @@
}
@Test public void explicitDirectProxy() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator,
- NO_PROXY, protocols, connectionSpecs, proxySelector);
- client.setProxy(NO_PROXY);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ Address address = new Address(uriHost, uriPort, dns, socketFactory, null, null, null,
+ authenticator, NO_PROXY, protocols, connectionSpecs, proxySelector);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(uriHost);
@@ -177,12 +147,12 @@
Address address = httpAddress();
proxySelector.proxies = null;
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -190,16 +160,16 @@
@Test public void proxySelectorReturnsNoProxies() throws Exception {
Address address = httpAddress();
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(uriHost);
- proxySelector.assertRequests(httpRequest.uri());
+ proxySelector.assertRequests(address.url().uri());
}
@Test public void proxySelectorReturnsMultipleProxies() throws Exception {
@@ -207,26 +177,26 @@
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
// First try the IP addresses of the first proxy, in sequence.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
dns.assertRequests(proxyAHost);
// Next try the IP address of the second proxy.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(254, 1);
- assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort);
+ dns.addresses(makeFakeAddresses(254, 1));
+ assertRoute(routeSelector.next(), address, proxyB, dns.address(0), proxyBPort);
dns.assertRequests(proxyBHost);
// Finally try the only IP address of the origin server.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(253, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(253, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -236,13 +206,13 @@
Address address = httpAddress();
proxySelector.proxies.add(NO_PROXY);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
// Only the origin server will be attempted.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -254,16 +224,16 @@
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
proxySelector.proxies.add(proxyA);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = null;
+ dns.unknownHost();
try {
routeSelector.next();
fail();
@@ -272,13 +242,13 @@
dns.assertRequests(proxyBHost);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(254, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(254, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -288,36 +258,35 @@
Address address = httpsAddress();
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
// Proxy A
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
dns.assertRequests(proxyAHost);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
// Proxy B
- dns.inetAddresses = makeFakeAddresses(254, 2);
- assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort);
+ dns.addresses(makeFakeAddresses(254, 2));
+ assertRoute(routeSelector.next(), address, proxyB, dns.address(0), proxyBPort);
dns.assertRequests(proxyBHost);
- assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort);
+ assertRoute(routeSelector.next(), address, proxyB, dns.address(1), proxyBPort);
// Origin
- dns.inetAddresses = makeFakeAddresses(253, 2);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(253, 2));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
assertFalse(routeSelector.hasNext());
}
@Test public void failedRoutesAreLast() throws Exception {
Address address = httpsAddress();
- client.setProxy(Proxy.NO_PROXY);
- RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
final int numberOfAddresses = 2;
- dns.inetAddresses = makeFakeAddresses(255, numberOfAddresses);
+ dns.addresses(makeFakeAddresses(255, numberOfAddresses));
// Extract the regular sequence of routes from selector.
List<Route> regularRoutes = new ArrayList<>();
@@ -330,7 +299,7 @@
// Add first regular route as failed.
routeDatabase.failed(regularRoutes.get(0));
// Reset selector
- routeSelector = RouteSelector.get(address, httpsRequest, client);
+ routeSelector = new RouteSelector(address, routeDatabase);
List<Route> routesWithFailedRoute = new ArrayList<>();
while (routeSelector.hasNext()) {
@@ -370,41 +339,25 @@
/** Returns an address that's without an SSL socket factory or hostname verifier. */
private Address httpAddress() {
- return new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator, null,
+ return new Address(uriHost, uriPort, dns, socketFactory, null, null, null, authenticator, null,
protocols, connectionSpecs, proxySelector);
}
private Address httpsAddress() {
- return new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+ return new Address(uriHost, uriPort, dns, socketFactory, sslSocketFactory,
hostnameVerifier, null, authenticator, null, protocols, connectionSpecs, proxySelector);
}
- private static InetAddress[] makeFakeAddresses(int prefix, int count) {
+ private static List<InetAddress> makeFakeAddresses(int prefix, int count) {
try {
- InetAddress[] result = new InetAddress[count];
+ List<InetAddress> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
- result[i] =
- InetAddress.getByAddress(new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i });
+ result.add(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 Network {
- List<String> requestedHosts = new ArrayList<>();
- InetAddress[] inetAddresses;
-
- @Override public InetAddress[] resolveInetAddresses(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();
- }
- }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java
new file mode 100644
index 0000000..951d9e6
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import com.squareup.okhttp.internal.HeldCertificate;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class CertificateChainCleanerTest {
+ @Test public void normalizeSingleSelfSignedCertificate() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(root), council.clean(list(root)));
+ }
+
+ @Test public void normalizeUnknownSelfSignedCertificate() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex());
+
+ try {
+ council.clean(list(root));
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ }
+ }
+
+ @Test public void orderedChainOfCertificatesWithRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certB, certA, root), council.clean(list(certB, certA, root)));
+ }
+
+ @Test public void orderedChainOfCertificatesWithoutRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certB, certA, root), council.clean(list(certB, certA))); // Root is added!
+ }
+
+ @Test public void unorderedChainOfCertificatesWithRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+ HeldCertificate certC = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .issuedBy(certB)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certC, certB, certA, root), council.clean(list(certC, certA, root, certB)));
+ }
+
+ @Test public void unorderedChainOfCertificatesWithoutRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+ HeldCertificate certC = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .issuedBy(certB)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certC, certB, certA, root), council.clean(list(certC, certA, certB)));
+ }
+
+ @Test public void unrelatedCertificatesAreOmitted() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+ HeldCertificate certUnnecessary = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certB, certA, root),
+ council.clean(list(certB, certUnnecessary, certA, root)));
+ }
+
+ @Test public void chainGoesAllTheWayToSelfSignedRoot() throws Exception {
+ HeldCertificate selfSigned = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate trusted = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(selfSigned)
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(trusted)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .issuedBy(certA)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(selfSigned.certificate, trusted.certificate));
+ assertEquals(list(certB, certA, trusted, selfSigned),
+ council.clean(list(certB, certA)));
+ assertEquals(list(certB, certA, trusted, selfSigned),
+ council.clean(list(certB, certA, trusted)));
+ assertEquals(list(certB, certA, trusted, selfSigned),
+ council.clean(list(certB, certA, trusted, selfSigned)));
+ }
+
+ @Test public void trustedRootNotSelfSigned() throws Exception {
+ HeldCertificate unknownSigner = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate trusted = new HeldCertificate.Builder()
+ .issuedBy(unknownSigner)
+ .serialNumber("2")
+ .build();
+ HeldCertificate intermediateCa = new HeldCertificate.Builder()
+ .issuedBy(trusted)
+ .serialNumber("3")
+ .build();
+ HeldCertificate certificate = new HeldCertificate.Builder()
+ .issuedBy(intermediateCa)
+ .serialNumber("4")
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(trusted.certificate));
+ assertEquals(list(certificate, intermediateCa, trusted),
+ council.clean(list(certificate, intermediateCa)));
+ assertEquals(list(certificate, intermediateCa, trusted),
+ council.clean(list(certificate, intermediateCa, trusted)));
+ }
+
+ @Test public void chainMaxLength() throws Exception {
+ List<HeldCertificate> heldCertificates = chainOfLength(10);
+ List<Certificate> certificates = new ArrayList<>();
+ for (HeldCertificate heldCertificate : heldCertificates) {
+ certificates.add(heldCertificate.certificate);
+ }
+
+ X509Certificate root = heldCertificates.get(heldCertificates.size() - 1).certificate;
+ CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex(root));
+ assertEquals(certificates, council.clean(certificates));
+ assertEquals(certificates, council.clean(certificates.subList(0, 9)));
+ }
+
+ @Test public void chainTooLong() throws Exception {
+ List<HeldCertificate> heldCertificates = chainOfLength(11);
+ List<Certificate> certificates = new ArrayList<>();
+ for (HeldCertificate heldCertificate : heldCertificates) {
+ certificates.add(heldCertificate.certificate);
+ }
+
+ X509Certificate root = heldCertificates.get(heldCertificates.size() - 1).certificate;
+ CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex(root));
+ try {
+ council.clean(certificates);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ }
+ }
+
+ /** Returns a chain starting at the leaf certificate and progressing to the root. */
+ private List<HeldCertificate> chainOfLength(int length) throws GeneralSecurityException {
+ List<HeldCertificate> result = new ArrayList<>();
+ for (int i = 1; i <= length; i++) {
+ result.add(0, new HeldCertificate.Builder()
+ .issuedBy(!result.isEmpty() ? result.get(0) : null)
+ .serialNumber(Integer.toString(i))
+ .build());
+ }
+ return result;
+ }
+
+ private List<Certificate> list(HeldCertificate... heldCertificates) {
+ List<Certificate> result = new ArrayList<>();
+ for (HeldCertificate heldCertificate : heldCertificates) {
+ result.add(heldCertificate.certificate);
+ }
+ return result;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java
new file mode 100644
index 0000000..5144dd2
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.HeldCertificate;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class CertificatePinnerChainValidationTest {
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ /** The pinner should pull the root certificate from the trust manager. */
+ @Test public void pinRootNotPresentInChain() throws Exception {
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+ HeldCertificate intermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("2")
+ .commonName("intermediate_ca")
+ .build();
+ HeldCertificate certificate = new HeldCertificate.Builder()
+ .issuedBy(intermediateCa)
+ .serialNumber("3")
+ .commonName(server.getHostName())
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(rootCa.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(certificate, intermediateCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+
+ // The request should complete successfully.
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call1 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response1 = call1.execute();
+ assertEquals("abc", response1.body().string());
+
+ // Confirm that a second request also succeeds. This should detect caching problems.
+ server.enqueue(new MockResponse()
+ .setBody("def")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call2 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response2 = call2.execute();
+ assertEquals("def", response2.body().string());
+ }
+
+ /** The pinner should accept an intermediate from the server's chain. */
+ @Test public void pinIntermediatePresentInChain() throws Exception {
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+ HeldCertificate intermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("2")
+ .commonName("intermediate_ca")
+ .build();
+ HeldCertificate certificate = new HeldCertificate.Builder()
+ .issuedBy(intermediateCa)
+ .serialNumber("3")
+ .commonName(server.getHostName())
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(intermediateCa.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(certificate, intermediateCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+
+ // The request should complete successfully.
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call1 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response1 = call1.execute();
+ assertEquals("abc", response1.body().string());
+
+ // Confirm that a second request also succeeds. This should detect caching problems.
+ server.enqueue(new MockResponse()
+ .setBody("def")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call2 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response2 = call2.execute();
+ assertEquals("def", response2.body().string());
+ }
+
+ @Test public void unrelatedPinnedLeafCertificateInChain() throws Exception {
+ // Start with a trusted root CA certificate.
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+
+ // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
+ // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
+ // certificate.
+ HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("2")
+ .commonName("good_intermediate_ca")
+ .build();
+ HeldCertificate goodCertificate = new HeldCertificate.Builder()
+ .issuedBy(goodIntermediateCa)
+ .serialNumber("3")
+ .commonName(server.getHostName())
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(goodCertificate.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ // Add a bad intermediate CA and have that issue a rogue certificate for localhost. Prepare
+ // an SSL context for an attacking webserver. It includes both these rogue certificates plus the
+ // trusted good certificate above. The attack is that by including the good certificate in the
+ // chain, we may trick the certificate pinner into accepting the rouge certificate.
+ HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("4")
+ .commonName("bad_intermediate_ca")
+ .build();
+ HeldCertificate rogueCertificate = new HeldCertificate.Builder()
+ .serialNumber("5")
+ .issuedBy(compromisedIntermediateCa)
+ .commonName(server.getHostName())
+ .build();
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(rogueCertificate, compromisedIntermediateCa, goodCertificate, rootCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ // Make a request from client to server. It should succeed certificate checks (unfortunately the
+ // rogue CA is trusted) but it should fail certificate pinning.
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Call call = client.newCall(request);
+ try {
+ call.execute();
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // Certificate pinning fails!
+ String message = expected.getMessage();
+ assertTrue(message, message.startsWith("Certificate pinning failure!"));
+ }
+ }
+
+ @Test public void unrelatedPinnedIntermediateCertificateInChain() throws Exception {
+ // Start with two root CA certificates, one is good and the other is compromised.
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+ HeldCertificate compromisedRootCa = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .ca(3)
+ .commonName("compromised_root")
+ .build();
+
+ // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
+ // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
+ // certificate.
+ HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("3")
+ .commonName("intermediate_ca")
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(goodIntermediateCa.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .addTrustedCertificate(compromisedRootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ // The attacker compromises the root CA, issues an intermediate with the same common name
+ // "intermediate_ca" as the good CA. This signs a rogue certificate for localhost. The server
+ // serves the good CAs certificate in the chain, which means the certificate pinner sees a
+ // different set of certificates than the SSL verifier.
+ HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(compromisedRootCa)
+ .ca(2)
+ .serialNumber("4")
+ .commonName("intermediate_ca")
+ .build();
+ HeldCertificate rogueCertificate = new HeldCertificate.Builder()
+ .serialNumber("5")
+ .issuedBy(compromisedIntermediateCa)
+ .commonName(server.getHostName())
+ .build();
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(
+ rogueCertificate, goodIntermediateCa, compromisedIntermediateCa, compromisedRootCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ // Make a request from client to server. It should succeed certificate checks (unfortunately the
+ // rogue CA is trusted) but it should fail certificate pinning.
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Call call = client.newCall(request);
+ try {
+ call.execute();
+ fail();
+ } catch (SSLHandshakeException expected) {
+ // On Android, the handshake fails before the certificate pinner runs.
+ String message = expected.getMessage();
+ assertTrue(message, message.contains("Could not validate certificate"));
+ } catch (SSLPeerUnverifiedException expected) {
+ // On OpenJDK, the handshake succeeds but the certificate pinner fails.
+ String message = expected.getMessage();
+ assertTrue(message, message.startsWith("Certificate pinning failure!"));
+ }
+ }
+}
diff --git a/okhttp-urlconnection/pom.xml b/okhttp-urlconnection/pom.xml
index be60560..c11c67f 100644
--- a/okhttp-urlconnection/pom.xml
+++ b/okhttp-urlconnection/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-urlconnection</artifactId>
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
index 454f6c3..2398de5 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
@@ -40,6 +40,7 @@
import com.squareup.okhttp.internal.http.RetryableSink;
import com.squareup.okhttp.internal.http.RouteException;
import com.squareup.okhttp.internal.http.StatusLine;
+import com.squareup.okhttp.internal.http.StreamAllocation;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -73,7 +74,7 @@
*
* <h3>What does 'connected' mean?</h3>
* This class inherits a {@code connected} field from the superclass. That field
- * is <strong>not</strong> used to indicate not whether this URLConnection is
+ * is <strong>not</strong> used to indicate whether this URLConnection is
* currently connected. Instead, it indicates whether a connection has ever been
* attempted. Once a connection has been attempted, certain properties (request
* header fields, request method, etc.) are immutable.
@@ -131,7 +132,7 @@
// Calling disconnect() before a connection exists should have no effect.
if (httpEngine == null) return;
- httpEngine.disconnect();
+ httpEngine.cancel();
// This doesn't close the stream because doing so would require all stream
// access to be synchronized. It's expected that the thread using the
@@ -161,9 +162,9 @@
if (responseHeaders == null) {
Response response = getResponse().getResponse();
Headers headers = response.headers();
-
responseHeaders = headers.newBuilder()
- .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response))
+ .add(OkHeaders.SELECTED_PROTOCOL, response.protocol().toString())
+ .add(OkHeaders.RESPONSE_SOURCE, responseSourceHeader(response))
.build();
}
return responseHeaders;
@@ -335,8 +336,9 @@
}
}
- private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody,
- Response priorResponse) throws MalformedURLException, UnknownHostException {
+ private HttpEngine newHttpEngine(String method, StreamAllocation streamAllocation,
+ RetryableSink requestBody, Response priorResponse)
+ throws MalformedURLException, UnknownHostException {
// OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
RequestBody placeholderBody = HttpMethod.requiresRequestBody(method)
? EMPTY_REQUEST_BODY
@@ -380,7 +382,7 @@
engineClient = client.clone().setCache(null);
}
- return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null,
+ return new HttpEngine(engineClient, request, bufferRequestBody, true, false, streamAllocation,
requestBody, priorResponse);
}
@@ -410,7 +412,7 @@
Request followUp = httpEngine.followUpRequest();
if (followUp == null) {
- httpEngine.releaseConnection();
+ httpEngine.releaseStreamAllocation();
return httpEngine;
}
@@ -434,12 +436,13 @@
throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
}
+ StreamAllocation streamAllocation = httpEngine.close();
if (!httpEngine.sameConnection(followUp.httpUrl())) {
- httpEngine.releaseConnection();
+ streamAllocation.release();
+ streamAllocation = null;
}
- Connection connection = httpEngine.close();
- httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody,
+ httpEngine = newHttpEngine(followUp.method(), streamAllocation, (RetryableSink) requestBody,
response);
}
}
@@ -450,18 +453,24 @@
* retried. Throws an exception if the request failed permanently.
*/
private boolean execute(boolean readResponse) throws IOException {
+ boolean releaseConnection = true;
if (urlFilter != null) {
urlFilter.checkURLPermitted(httpEngine.getRequest().url());
}
try {
httpEngine.sendRequest();
- route = httpEngine.getRoute();
- handshake = httpEngine.getConnection() != null
- ? httpEngine.getConnection().getHandshake()
- : null;
+ Connection connection = httpEngine.getConnection();
+ if (connection != null) {
+ route = connection.getRoute();
+ handshake = connection.getHandshake();
+ } else {
+ route = null;
+ handshake = null;
+ }
if (readResponse) {
httpEngine.readResponse();
}
+ releaseConnection = false;
return true;
} catch (RequestException e) {
@@ -473,6 +482,7 @@
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = httpEngine.recover(e);
if (retryEngine != null) {
+ releaseConnection = false;
httpEngine = retryEngine;
return false;
}
@@ -485,6 +495,7 @@
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = httpEngine.recover(e);
if (retryEngine != null) {
+ releaseConnection = false;
httpEngine = retryEngine;
return false;
}
@@ -492,6 +503,12 @@
// Give up; recovery is not possible.
httpEngineFailure = e;
throw e;
+ } finally {
+ // We're throwing an unchecked exception. Release any resources.
+ if (releaseConnection) {
+ StreamAllocation streamAllocation = httpEngine.close();
+ streamAllocation.release();
+ }
}
}
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
index 27e4d23..140b4d2 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
@@ -68,19 +68,15 @@
return delegate.client.getSslSocketFactory();
}
- // ANDROID-BEGIN
- // @Override public long getContentLengthLong() {
- // return delegate.getContentLengthLong();
- // }
- // ANDROID-END
+ @Override public long getContentLengthLong() {
+ return delegate.getContentLengthLong();
+ }
@Override public void setFixedLengthStreamingMode(long contentLength) {
delegate.setFixedLengthStreamingMode(contentLength);
}
- // ANDROID-BEGIN
- // @Override public long getHeaderFieldLong(String field, long defaultValue) {
- // return delegate.getHeaderFieldLong(field, defaultValue);
- // }
- // ANDROID-END
+ @Override public long getHeaderFieldLong(String field, long defaultValue) {
+ return delegate.getHeaderFieldLong(field, defaultValue);
+ }
}
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
index 983dd57..7e47918 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
@@ -1,5 +1,6 @@
package com.squareup.okhttp;
+import com.squareup.okhttp.internal.http.OkHeaders;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.URLFilter;
import com.squareup.okhttp.internal.io.FileSystem;
@@ -16,6 +17,8 @@
import java.util.Locale;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
+import okio.BufferedSource;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -28,16 +31,22 @@
public class OkUrlFactoryTest {
@Rule public MockWebServer server = new MockWebServer();
+ @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
- private FileSystem fileSystem = new InMemoryFileSystem();
private OkUrlFactory factory;
+ private Cache cache;
@Before public void setUp() throws IOException {
OkHttpClient client = new OkHttpClient();
- client.setCache(new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem));
+ cache = new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem);
+ client.setCache(cache);
factory = new OkUrlFactory(client);
}
+ @After public void tearDown() throws IOException {
+ cache.delete();
+ }
+
/**
* Response code 407 should only come from proxy servers. Android's client
* throws if it is sent by an origin server.
@@ -66,6 +75,7 @@
HttpURLConnection connection = factory.open(server.getUrl("/"));
assertResponseHeader(connection, "NETWORK 404");
+ connection.getErrorStream().close();
}
@Test public void conditionalCacheHitResponseSourceHeaders() throws Exception {
@@ -166,13 +176,14 @@
}
private void assertResponseBody(HttpURLConnection connection, String expected) throws Exception {
- String actual = buffer(source(connection.getInputStream())).readString(US_ASCII);
+ BufferedSource source = buffer(source(connection.getInputStream()));
+ String actual = source.readString(US_ASCII);
+ source.close();
assertEquals(expected, actual);
}
private void assertResponseHeader(HttpURLConnection connection, String expected) {
- final String headerFieldPrefix = Platform.get().getPrefix();
- assertEquals(expected, connection.getHeaderField(headerFieldPrefix + "-Response-Source"));
+ assertEquals(expected, connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
}
private void assertResponseCode(HttpURLConnection connection, int expected) throws IOException {
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
index 0af815b..66bf7c2 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
@@ -19,7 +19,6 @@
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.io.FileSystem;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -81,9 +80,9 @@
@Rule public MockWebServer server = new MockWebServer();
@Rule public MockWebServer server2 = new MockWebServer();
+ @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
private final SSLContext sslContext = SslContextBuilder.localhost();
- private final FileSystem fileSystem = new InMemoryFileSystem();
private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
private Cache cache;
private final CookieManager cookieManager = new CookieManager();
@@ -98,6 +97,7 @@
@After public void tearDown() throws Exception {
ResponseCache.setDefault(null);
CookieHandler.setDefault(null);
+ cache.delete();
}
@Test public void responseCacheAccessWithOkHttpMember() throws IOException {
@@ -855,7 +855,7 @@
assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals("A", readAscii(client.open(server.getUrl("/"))));
- assertEquals(1, client.client().getConnectionPool().getConnectionCount());
+ assertEquals(1, client.client().getConnectionPool().getIdleConnectionCount());
}
@Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -1586,6 +1586,7 @@
HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("A", connection.getHeaderField(""));
+ assertEquals("body", readAscii(connection));
}
/**
diff --git a/okhttp-ws-tests/fuzzingserver-config.json b/okhttp-ws-tests/fuzzingserver-config.json
new file mode 100644
index 0000000..99e06ab
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-config.json
@@ -0,0 +1,153 @@
+{
+ "url": "ws://127.0.0.1:9001",
+ "outdir": "./target/fuzzingserver-report",
+ "cases": ["*"],
+ "exclude-cases": [
+ "6.1.1",
+ "6.1.2",
+ "6.1.3",
+ "6.2.1",
+ "6.2.2",
+ "6.2.3",
+ "6.2.4",
+ "6.3.1",
+ "6.3.2",
+ "6.4.1",
+ "6.4.2",
+ "6.4.3",
+ "6.4.4",
+ "6.5.1",
+ "6.5.2",
+ "6.5.3",
+ "6.5.4",
+ "6.5.5",
+ "6.6.1",
+ "6.6.2",
+ "6.6.3",
+ "6.6.4",
+ "6.6.5",
+ "6.6.6",
+ "6.6.7",
+ "6.6.8",
+ "6.6.9",
+ "6.6.10",
+ "6.6.11",
+ "6.7.1",
+ "6.7.2",
+ "6.7.3",
+ "6.7.4",
+ "6.8.1",
+ "6.8.2",
+ "6.9.1",
+ "6.9.2",
+ "6.9.3",
+ "6.9.4",
+ "6.10.1",
+ "6.10.2",
+ "6.10.3",
+ "6.11.1",
+ "6.11.2",
+ "6.11.3",
+ "6.11.4",
+ "6.11.5",
+ "6.12.1",
+ "6.12.2",
+ "6.12.3",
+ "6.12.4",
+ "6.12.5",
+ "6.12.6",
+ "6.12.7",
+ "6.12.8",
+ "6.13.1",
+ "6.13.2",
+ "6.13.3",
+ "6.13.4",
+ "6.13.5",
+ "6.14.1",
+ "6.14.2",
+ "6.14.3",
+ "6.14.4",
+ "6.14.5",
+ "6.14.6",
+ "6.14.7",
+ "6.14.8",
+ "6.14.9",
+ "6.14.10",
+ "6.15.1",
+ "6.16.1",
+ "6.16.2",
+ "6.16.3",
+ "6.17.1",
+ "6.17.2",
+ "6.17.3",
+ "6.17.4",
+ "6.17.5",
+ "6.18.1",
+ "6.18.2",
+ "6.18.3",
+ "6.18.4",
+ "6.18.5",
+ "6.19.1",
+ "6.19.2",
+ "6.19.3",
+ "6.19.4",
+ "6.19.5",
+ "6.20.1",
+ "6.20.2",
+ "6.20.3",
+ "6.20.4",
+ "6.20.5",
+ "6.20.6",
+ "6.20.7",
+ "6.21.1",
+ "6.21.2",
+ "6.21.3",
+ "6.21.4",
+ "6.21.5",
+ "6.21.6",
+ "6.21.7",
+ "6.21.8",
+ "6.22.1",
+ "6.22.2",
+ "6.22.3",
+ "6.22.4",
+ "6.22.5",
+ "6.22.6",
+ "6.22.7",
+ "6.22.8",
+ "6.22.9",
+ "6.22.10",
+ "6.22.11",
+ "6.22.12",
+ "6.22.13",
+ "6.22.14",
+ "6.22.15",
+ "6.22.16",
+ "6.22.17",
+ "6.22.18",
+ "6.22.19",
+ "6.22.20",
+ "6.22.21",
+ "6.22.22",
+ "6.22.23",
+ "6.22.24",
+ "6.22.25",
+ "6.22.26",
+ "6.22.27",
+ "6.22.28",
+ "6.22.29",
+ "6.22.30",
+ "6.22.31",
+ "6.22.32",
+ "6.22.33",
+ "6.22.34",
+ "6.23.1",
+ "6.23.2",
+ "6.23.3",
+ "6.23.4",
+ "6.23.5",
+ "6.23.6",
+ "6.23.7"
+ ],
+ "exclude-agent-cases": {}
+}
diff --git a/okhttp-ws-tests/fuzzingserver-expected.txt b/okhttp-ws-tests/fuzzingserver-expected.txt
new file mode 100644
index 0000000..f4a3305
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-expected.txt
@@ -0,0 +1,376 @@
+"1.1.1 OK"
+"1.1.2 OK"
+"1.1.3 OK"
+"1.1.4 OK"
+"1.1.5 OK"
+"1.1.6 OK"
+"1.1.7 OK"
+"1.1.8 OK"
+"1.2.1 OK"
+"1.2.2 OK"
+"1.2.3 OK"
+"1.2.4 OK"
+"1.2.5 OK"
+"1.2.6 OK"
+"1.2.7 OK"
+"1.2.8 OK"
+"10.1.1 OK"
+"12.1.1 UNIMPLEMENTED"
+"12.1.10 UNIMPLEMENTED"
+"12.1.11 UNIMPLEMENTED"
+"12.1.12 UNIMPLEMENTED"
+"12.1.13 UNIMPLEMENTED"
+"12.1.14 UNIMPLEMENTED"
+"12.1.15 UNIMPLEMENTED"
+"12.1.16 UNIMPLEMENTED"
+"12.1.17 UNIMPLEMENTED"
+"12.1.18 UNIMPLEMENTED"
+"12.1.2 UNIMPLEMENTED"
+"12.1.3 UNIMPLEMENTED"
+"12.1.4 UNIMPLEMENTED"
+"12.1.5 UNIMPLEMENTED"
+"12.1.6 UNIMPLEMENTED"
+"12.1.7 UNIMPLEMENTED"
+"12.1.8 UNIMPLEMENTED"
+"12.1.9 UNIMPLEMENTED"
+"12.2.1 UNIMPLEMENTED"
+"12.2.10 UNIMPLEMENTED"
+"12.2.11 UNIMPLEMENTED"
+"12.2.12 UNIMPLEMENTED"
+"12.2.13 UNIMPLEMENTED"
+"12.2.14 UNIMPLEMENTED"
+"12.2.15 UNIMPLEMENTED"
+"12.2.16 UNIMPLEMENTED"
+"12.2.17 UNIMPLEMENTED"
+"12.2.18 UNIMPLEMENTED"
+"12.2.2 UNIMPLEMENTED"
+"12.2.3 UNIMPLEMENTED"
+"12.2.4 UNIMPLEMENTED"
+"12.2.5 UNIMPLEMENTED"
+"12.2.6 UNIMPLEMENTED"
+"12.2.7 UNIMPLEMENTED"
+"12.2.8 UNIMPLEMENTED"
+"12.2.9 UNIMPLEMENTED"
+"12.3.1 UNIMPLEMENTED"
+"12.3.10 UNIMPLEMENTED"
+"12.3.11 UNIMPLEMENTED"
+"12.3.12 UNIMPLEMENTED"
+"12.3.13 UNIMPLEMENTED"
+"12.3.14 UNIMPLEMENTED"
+"12.3.15 UNIMPLEMENTED"
+"12.3.16 UNIMPLEMENTED"
+"12.3.17 UNIMPLEMENTED"
+"12.3.18 UNIMPLEMENTED"
+"12.3.2 UNIMPLEMENTED"
+"12.3.3 UNIMPLEMENTED"
+"12.3.4 UNIMPLEMENTED"
+"12.3.5 UNIMPLEMENTED"
+"12.3.6 UNIMPLEMENTED"
+"12.3.7 UNIMPLEMENTED"
+"12.3.8 UNIMPLEMENTED"
+"12.3.9 UNIMPLEMENTED"
+"12.4.1 UNIMPLEMENTED"
+"12.4.10 UNIMPLEMENTED"
+"12.4.11 UNIMPLEMENTED"
+"12.4.12 UNIMPLEMENTED"
+"12.4.13 UNIMPLEMENTED"
+"12.4.14 UNIMPLEMENTED"
+"12.4.15 UNIMPLEMENTED"
+"12.4.16 UNIMPLEMENTED"
+"12.4.17 UNIMPLEMENTED"
+"12.4.18 UNIMPLEMENTED"
+"12.4.2 UNIMPLEMENTED"
+"12.4.3 UNIMPLEMENTED"
+"12.4.4 UNIMPLEMENTED"
+"12.4.5 UNIMPLEMENTED"
+"12.4.6 UNIMPLEMENTED"
+"12.4.7 UNIMPLEMENTED"
+"12.4.8 UNIMPLEMENTED"
+"12.4.9 UNIMPLEMENTED"
+"12.5.1 UNIMPLEMENTED"
+"12.5.10 UNIMPLEMENTED"
+"12.5.11 UNIMPLEMENTED"
+"12.5.12 UNIMPLEMENTED"
+"12.5.13 UNIMPLEMENTED"
+"12.5.14 UNIMPLEMENTED"
+"12.5.15 UNIMPLEMENTED"
+"12.5.16 UNIMPLEMENTED"
+"12.5.17 UNIMPLEMENTED"
+"12.5.18 UNIMPLEMENTED"
+"12.5.2 UNIMPLEMENTED"
+"12.5.3 UNIMPLEMENTED"
+"12.5.4 UNIMPLEMENTED"
+"12.5.5 UNIMPLEMENTED"
+"12.5.6 UNIMPLEMENTED"
+"12.5.7 UNIMPLEMENTED"
+"12.5.8 UNIMPLEMENTED"
+"12.5.9 UNIMPLEMENTED"
+"13.1.1 UNIMPLEMENTED"
+"13.1.10 UNIMPLEMENTED"
+"13.1.11 UNIMPLEMENTED"
+"13.1.12 UNIMPLEMENTED"
+"13.1.13 UNIMPLEMENTED"
+"13.1.14 UNIMPLEMENTED"
+"13.1.15 UNIMPLEMENTED"
+"13.1.16 UNIMPLEMENTED"
+"13.1.17 UNIMPLEMENTED"
+"13.1.18 UNIMPLEMENTED"
+"13.1.2 UNIMPLEMENTED"
+"13.1.3 UNIMPLEMENTED"
+"13.1.4 UNIMPLEMENTED"
+"13.1.5 UNIMPLEMENTED"
+"13.1.6 UNIMPLEMENTED"
+"13.1.7 UNIMPLEMENTED"
+"13.1.8 UNIMPLEMENTED"
+"13.1.9 UNIMPLEMENTED"
+"13.2.1 UNIMPLEMENTED"
+"13.2.10 UNIMPLEMENTED"
+"13.2.11 UNIMPLEMENTED"
+"13.2.12 UNIMPLEMENTED"
+"13.2.13 UNIMPLEMENTED"
+"13.2.14 UNIMPLEMENTED"
+"13.2.15 UNIMPLEMENTED"
+"13.2.16 UNIMPLEMENTED"
+"13.2.17 UNIMPLEMENTED"
+"13.2.18 UNIMPLEMENTED"
+"13.2.2 UNIMPLEMENTED"
+"13.2.3 UNIMPLEMENTED"
+"13.2.4 UNIMPLEMENTED"
+"13.2.5 UNIMPLEMENTED"
+"13.2.6 UNIMPLEMENTED"
+"13.2.7 UNIMPLEMENTED"
+"13.2.8 UNIMPLEMENTED"
+"13.2.9 UNIMPLEMENTED"
+"13.3.1 UNIMPLEMENTED"
+"13.3.10 UNIMPLEMENTED"
+"13.3.11 UNIMPLEMENTED"
+"13.3.12 UNIMPLEMENTED"
+"13.3.13 UNIMPLEMENTED"
+"13.3.14 UNIMPLEMENTED"
+"13.3.15 UNIMPLEMENTED"
+"13.3.16 UNIMPLEMENTED"
+"13.3.17 UNIMPLEMENTED"
+"13.3.18 UNIMPLEMENTED"
+"13.3.2 UNIMPLEMENTED"
+"13.3.3 UNIMPLEMENTED"
+"13.3.4 UNIMPLEMENTED"
+"13.3.5 UNIMPLEMENTED"
+"13.3.6 UNIMPLEMENTED"
+"13.3.7 UNIMPLEMENTED"
+"13.3.8 UNIMPLEMENTED"
+"13.3.9 UNIMPLEMENTED"
+"13.4.1 UNIMPLEMENTED"
+"13.4.10 UNIMPLEMENTED"
+"13.4.11 UNIMPLEMENTED"
+"13.4.12 UNIMPLEMENTED"
+"13.4.13 UNIMPLEMENTED"
+"13.4.14 UNIMPLEMENTED"
+"13.4.15 UNIMPLEMENTED"
+"13.4.16 UNIMPLEMENTED"
+"13.4.17 UNIMPLEMENTED"
+"13.4.18 UNIMPLEMENTED"
+"13.4.2 UNIMPLEMENTED"
+"13.4.3 UNIMPLEMENTED"
+"13.4.4 UNIMPLEMENTED"
+"13.4.5 UNIMPLEMENTED"
+"13.4.6 UNIMPLEMENTED"
+"13.4.7 UNIMPLEMENTED"
+"13.4.8 UNIMPLEMENTED"
+"13.4.9 UNIMPLEMENTED"
+"13.5.1 UNIMPLEMENTED"
+"13.5.10 UNIMPLEMENTED"
+"13.5.11 UNIMPLEMENTED"
+"13.5.12 UNIMPLEMENTED"
+"13.5.13 UNIMPLEMENTED"
+"13.5.14 UNIMPLEMENTED"
+"13.5.15 UNIMPLEMENTED"
+"13.5.16 UNIMPLEMENTED"
+"13.5.17 UNIMPLEMENTED"
+"13.5.18 UNIMPLEMENTED"
+"13.5.2 UNIMPLEMENTED"
+"13.5.3 UNIMPLEMENTED"
+"13.5.4 UNIMPLEMENTED"
+"13.5.5 UNIMPLEMENTED"
+"13.5.6 UNIMPLEMENTED"
+"13.5.7 UNIMPLEMENTED"
+"13.5.8 UNIMPLEMENTED"
+"13.5.9 UNIMPLEMENTED"
+"13.6.1 UNIMPLEMENTED"
+"13.6.10 UNIMPLEMENTED"
+"13.6.11 UNIMPLEMENTED"
+"13.6.12 UNIMPLEMENTED"
+"13.6.13 UNIMPLEMENTED"
+"13.6.14 UNIMPLEMENTED"
+"13.6.15 UNIMPLEMENTED"
+"13.6.16 UNIMPLEMENTED"
+"13.6.17 UNIMPLEMENTED"
+"13.6.18 UNIMPLEMENTED"
+"13.6.2 UNIMPLEMENTED"
+"13.6.3 UNIMPLEMENTED"
+"13.6.4 UNIMPLEMENTED"
+"13.6.5 UNIMPLEMENTED"
+"13.6.6 UNIMPLEMENTED"
+"13.6.7 UNIMPLEMENTED"
+"13.6.8 UNIMPLEMENTED"
+"13.6.9 UNIMPLEMENTED"
+"13.7.1 UNIMPLEMENTED"
+"13.7.10 UNIMPLEMENTED"
+"13.7.11 UNIMPLEMENTED"
+"13.7.12 UNIMPLEMENTED"
+"13.7.13 UNIMPLEMENTED"
+"13.7.14 UNIMPLEMENTED"
+"13.7.15 UNIMPLEMENTED"
+"13.7.16 UNIMPLEMENTED"
+"13.7.17 UNIMPLEMENTED"
+"13.7.18 UNIMPLEMENTED"
+"13.7.2 UNIMPLEMENTED"
+"13.7.3 UNIMPLEMENTED"
+"13.7.4 UNIMPLEMENTED"
+"13.7.5 UNIMPLEMENTED"
+"13.7.6 UNIMPLEMENTED"
+"13.7.7 UNIMPLEMENTED"
+"13.7.8 UNIMPLEMENTED"
+"13.7.9 UNIMPLEMENTED"
+"2.1 OK"
+"2.10 OK"
+"2.11 OK"
+"2.2 OK"
+"2.3 OK"
+"2.4 OK"
+"2.5 OK"
+"2.6 OK"
+"2.7 OK"
+"2.8 OK"
+"2.9 OK"
+"3.1 OK"
+"3.2 NON-STRICT"
+"3.3 NON-STRICT"
+"3.4 NON-STRICT"
+"3.5 OK"
+"3.6 OK"
+"3.7 OK"
+"4.1.1 OK"
+"4.1.2 OK"
+"4.1.3 NON-STRICT"
+"4.1.4 NON-STRICT"
+"4.1.5 OK"
+"4.2.1 OK"
+"4.2.2 OK"
+"4.2.3 NON-STRICT"
+"4.2.4 NON-STRICT"
+"4.2.5 OK"
+"5.1 OK"
+"5.10 OK"
+"5.11 OK"
+"5.12 OK"
+"5.13 OK"
+"5.14 OK"
+"5.15 NON-STRICT"
+"5.16 OK"
+"5.17 OK"
+"5.18 OK"
+"5.19 OK"
+"5.2 OK"
+"5.20 OK"
+"5.3 OK"
+"5.4 OK"
+"5.5 OK"
+"5.6 OK"
+"5.7 OK"
+"5.8 OK"
+"5.9 OK"
+"7.1.1 OK"
+"7.1.2 OK"
+"7.1.3 OK"
+"7.1.4 OK"
+"7.1.5 FAILED"
+"7.1.6 INFORMATIONAL"
+"7.13.1 INFORMATIONAL"
+"7.13.2 INFORMATIONAL"
+"7.3.1 OK"
+"7.3.2 OK"
+"7.3.3 OK"
+"7.3.4 OK"
+"7.3.5 OK"
+"7.3.6 OK"
+"7.5.1 FAILED"
+"7.7.1 OK"
+"7.7.10 OK"
+"7.7.11 OK"
+"7.7.12 OK"
+"7.7.13 OK"
+"7.7.2 OK"
+"7.7.3 OK"
+"7.7.4 OK"
+"7.7.5 OK"
+"7.7.6 OK"
+"7.7.7 OK"
+"7.7.8 OK"
+"7.7.9 OK"
+"7.9.1 OK"
+"7.9.10 OK"
+"7.9.11 OK"
+"7.9.12 OK"
+"7.9.13 OK"
+"7.9.2 OK"
+"7.9.3 OK"
+"7.9.4 OK"
+"7.9.5 OK"
+"7.9.6 OK"
+"7.9.7 OK"
+"7.9.8 OK"
+"7.9.9 OK"
+"9.1.1 OK"
+"9.1.2 OK"
+"9.1.3 OK"
+"9.1.4 OK"
+"9.1.5 OK"
+"9.1.6 OK"
+"9.2.1 OK"
+"9.2.2 OK"
+"9.2.3 OK"
+"9.2.4 OK"
+"9.2.5 OK"
+"9.2.6 OK"
+"9.3.1 OK"
+"9.3.2 OK"
+"9.3.3 OK"
+"9.3.4 OK"
+"9.3.5 OK"
+"9.3.6 OK"
+"9.3.7 OK"
+"9.3.8 OK"
+"9.3.9 OK"
+"9.4.1 OK"
+"9.4.2 OK"
+"9.4.3 OK"
+"9.4.4 OK"
+"9.4.5 OK"
+"9.4.6 OK"
+"9.4.7 OK"
+"9.4.8 OK"
+"9.4.9 OK"
+"9.5.1 OK"
+"9.5.2 OK"
+"9.5.3 OK"
+"9.5.4 OK"
+"9.5.5 OK"
+"9.5.6 OK"
+"9.6.1 OK"
+"9.6.2 OK"
+"9.6.3 OK"
+"9.6.4 OK"
+"9.6.5 OK"
+"9.6.6 OK"
+"9.7.1 OK"
+"9.7.2 OK"
+"9.7.3 OK"
+"9.7.4 OK"
+"9.7.5 OK"
+"9.7.6 OK"
+"9.8.1 OK"
+"9.8.2 OK"
+"9.8.3 OK"
+"9.8.4 OK"
+"9.8.5 OK"
+"9.8.6 OK"
diff --git a/okhttp-ws-tests/fuzzingserver-test.sh b/okhttp-ws-tests/fuzzingserver-test.sh
new file mode 100755
index 0000000..af89a42
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-test.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+which wstest
+if [ $? != 0 ]; then
+ echo "Run 'pip install autobahntestsuite', maybe with 'sudo'."
+ exit 1
+fi
+which jq
+if [ $? != 0 ]; then
+ echo "Run 'brew install jq'"
+ exit 1
+fi
+
+trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
+
+set -ex
+
+wstest -m fuzzingserver -s fuzzingserver-config.json &
+sleep 2 # wait for wstest to start
+
+java -jar target/okhttp-ws-tests-*-jar-with-dependencies.jar
+
+jq '.[] as $in | $in | keys[] | . + " " + $in[.].behavior' target/fuzzingserver-report/index.json > target/fuzzingserver-actual.txt
+
+diff fuzzingserver-expected.txt target/fuzzingserver-actual.txt
diff --git a/okhttp-ws-tests/fuzzingserver-update-expected.sh b/okhttp-ws-tests/fuzzingserver-update-expected.sh
new file mode 100755
index 0000000..56592c9
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-update-expected.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+if [ ! -f target/fuzzingserver-actual.txt ]; then
+ echo "File not found. Did you run the Autobahn test script?"
+ exit 1
+fi
+
+cp target/fuzzingserver-actual.txt fuzzingserver-expected.txt
diff --git a/okhttp-ws-tests/pom.xml b/okhttp-ws-tests/pom.xml
index c7d1778..1802304 100644
--- a/okhttp-ws-tests/pom.xml
+++ b/okhttp-ws-tests/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-ws-tests</artifactId>
@@ -15,15 +15,16 @@
<dependencies>
<dependency>
<groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-ws</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp-testing-support</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
- <dependency>
- <groupId>com.squareup.okhttp</groupId>
- <artifactId>okhttp-ws</artifactId>
- <version>${project.version}</version>
- </dependency>
<dependency>
<groupId>junit</groupId>
@@ -40,6 +41,28 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ <archive>
+ <manifest>
+ <mainClass>com.squareup.okhttp.ws.AutobahnTester</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
<!-- Do not deploy this as an artifact to Maven central. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java b/okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
similarity index 77%
rename from okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
rename to okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
index a592624..08f7beb 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
+++ b/okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
@@ -17,7 +17,9 @@
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.Version;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
@@ -29,6 +31,9 @@
import okio.Buffer;
import okio.BufferedSource;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
+
/**
* Exercises the web socket implementation against the
* <a href="http://autobahn.ws/testsuite/">Autobahn Testsuite</a>.
@@ -64,28 +69,34 @@
private void runTest(final long number, final long count) throws IOException {
final CountDownLatch latch = new CountDownLatch(1);
- newWebSocket("/runCase?case=" + number + "&agent=" + Version.userAgent()) //
+ final AtomicLong startNanos = new AtomicLong();
+ newWebSocket("/runCase?case=" + number + "&agent=okhttp") //
.enqueue(new WebSocketListener() {
private final ExecutorService sendExecutor = Executors.newSingleThreadExecutor();
private WebSocket webSocket;
@Override public void onOpen(WebSocket webSocket, Response response) {
- System.out.println("Executing test case " + number + "/" + count);
this.webSocket = webSocket;
+
+ System.out.println("Executing test case " + number + "/" + count);
+ startNanos.set(System.nanoTime());
}
- @Override public void onMessage(BufferedSource payload, final WebSocket.PayloadType type)
- throws IOException {
- final Buffer buffer = new Buffer();
- payload.readAll(buffer);
- payload.close();
-
+ @Override public void onMessage(final ResponseBody message) throws IOException {
+ final RequestBody response;
+ if (message.contentType() == TEXT) {
+ response = RequestBody.create(TEXT, message.string());
+ } else {
+ BufferedSource source = message.source();
+ response = RequestBody.create(BINARY, source.readByteString());
+ source.close();
+ }
sendExecutor.execute(new Runnable() {
@Override public void run() {
try {
- webSocket.sendMessage(type, buffer);
+ webSocket.sendMessage(response);
} catch (IOException e) {
- e.printStackTrace();
+ e.printStackTrace(System.out);
}
}
});
@@ -100,16 +111,21 @@
}
@Override public void onFailure(IOException e, Response response) {
+ e.printStackTrace(System.out);
latch.countDown();
}
});
try {
- if (!latch.await(10, TimeUnit.SECONDS)) {
- throw new IllegalStateException("Timed out waiting for count.");
+ if (!latch.await(30, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timed out waiting for test " + number + " to finish.");
}
} catch (InterruptedException e) {
throw new AssertionError();
}
+
+ long endNanos = System.nanoTime();
+ long tookMs = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos.get());
+ System.out.println("Took " + tookMs + "ms");
}
private long getTestCount() throws IOException {
@@ -120,10 +136,9 @@
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
- countRef.set(payload.readDecimalLong());
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ countRef.set(message.source().readDecimalLong());
+ message.close();
}
@Override public void onPong(Buffer payload) {
@@ -158,8 +173,7 @@
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
+ @Override public void onMessage(ResponseBody message) throws IOException {
}
@Override public void onPong(Buffer payload) {
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
index 241376d..5cc0c25 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
@@ -15,23 +15,28 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ws.WebSocketRecorder;
+import java.io.Closeable;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Random;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
import okio.Buffer;
import okio.BufferedSink;
+import okio.BufferedSource;
import okio.ByteString;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@@ -42,34 +47,43 @@
// zero effect on the behavior of the WebSocket API which is why tests are only written once
// from the perspective of a single peer.
- private final Executor clientExecutor = Executors.newSingleThreadExecutor();
+ private final Executor clientExecutor = new SynchronousExecutor();
private RealWebSocket client;
private boolean clientConnectionCloseThrows;
private boolean clientConnectionClosed;
- private final Buffer client2Server = new Buffer();
+ private final MemorySocket client2Server = new MemorySocket();
private final WebSocketRecorder clientListener = new WebSocketRecorder();
- private final Executor serverExecutor = Executors.newSingleThreadExecutor();
+ private final Executor serverExecutor = new SynchronousExecutor();
private RealWebSocket server;
- private final Buffer server2client = new Buffer();
+ private boolean serverConnectionClosed;
+ private final MemorySocket server2client = new MemorySocket();
private final WebSocketRecorder serverListener = new WebSocketRecorder();
@Before public void setUp() {
Random random = new Random(0);
String url = "http://example.com/websocket";
- client = new RealWebSocket(true, server2client, client2Server, random, clientExecutor,
- clientListener, url) {
- @Override protected void closeConnection() throws IOException {
+ client = new RealWebSocket(true, server2client.source(), client2Server.sink(), random,
+ clientExecutor, clientListener, url) {
+ @Override protected void close() throws IOException {
+ if (clientConnectionClosed) {
+ throw new AssertionError("Already closed");
+ }
clientConnectionClosed = true;
+
if (clientConnectionCloseThrows) {
throw new IOException("Oops!");
}
}
};
- server = new RealWebSocket(false, client2Server, server2client, random, serverExecutor,
- serverListener, url) {
- @Override protected void closeConnection() throws IOException {
+ server = new RealWebSocket(false, client2Server.source(), server2client.sink(), random,
+ serverExecutor, serverListener, url) {
+ @Override protected void close() throws IOException {
+ if (serverConnectionClosed) {
+ throw new AssertionError("Already closed");
+ }
+ serverConnectionClosed = true;
}
};
}
@@ -79,36 +93,82 @@
serverListener.assertExhausted();
}
+ @Test public void nullMessageThrows() throws IOException {
+ try {
+ client.sendMessage(null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("message == null", e.getMessage());
+ }
+ }
+
@Test public void textMessage() throws IOException {
- client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
server.readMessage();
serverListener.assertTextMessage("Hello!");
}
@Test public void binaryMessage() throws IOException {
- client.sendMessage(BINARY, new Buffer().writeUtf8("Hello!"));
+ client.sendMessage(RequestBody.create(BINARY, "Hello!"));
server.readMessage();
serverListener.assertBinaryMessage(new byte[] { 'H', 'e', 'l', 'l', 'o', '!' });
}
+ @Test public void missingContentTypeThrows() throws IOException {
+ try {
+ client.sendMessage(RequestBody.create(null, "Hey!"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.",
+ e.getMessage());
+ }
+ }
+
+ @Test public void unknownContentTypeThrows() throws IOException {
+ try {
+ client.sendMessage(RequestBody.create(MediaType.parse("text/plain"), "Hey!"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals(
+ "Unknown message content type: text/plain. Must use WebSocket.TEXT or WebSocket.BINARY.",
+ e.getMessage());
+ }
+ }
+
@Test public void streamingMessage() throws IOException {
- BufferedSink sink = client.newMessageSink(TEXT);
- sink.writeUtf8("Hel").flush();
- sink.writeUtf8("lo!").flush();
- sink.close();
+ RequestBody message = new RequestBody() {
+ @Override public MediaType contentType() {
+ return TEXT;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Hel").flush();
+ sink.writeUtf8("lo!").flush();
+ sink.close();
+ }
+ };
+ client.sendMessage(message);
server.readMessage();
serverListener.assertTextMessage("Hello!");
}
@Test public void streamingMessageCanInterleavePing() throws IOException, InterruptedException {
- BufferedSink sink = client.newMessageSink(TEXT);
- sink.writeUtf8("Hel").flush();
- client.sendPing(new Buffer().writeUtf8("Pong?"));
- sink.writeUtf8("lo!").flush();
- sink.close();
+ RequestBody message = new RequestBody() {
+ @Override public MediaType contentType() {
+ return TEXT;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Hel").flush();
+ client.sendPing(new Buffer().writeUtf8("Pong?"));
+ sink.writeUtf8("lo!").flush();
+ sink.close();
+ }
+ };
+
+ client.sendMessage(message);
server.readMessage();
serverListener.assertTextMessage("Hello!");
- waitForExecutor(serverExecutor); // Pong write happens asynchronously.
client.readMessage();
clientListener.assertPong(new Buffer().writeUtf8("Pong?"));
}
@@ -116,7 +176,6 @@
@Test public void pingWritesPong() throws IOException, InterruptedException {
client.sendPing(new Buffer().writeUtf8("Hello!"));
server.readMessage(); // Read the ping, write the pong.
- waitForExecutor(serverExecutor); // Pong write happens asynchronously.
client.readMessage(); // Read the pong.
clientListener.assertPong(new Buffer().writeUtf8("Hello!"));
}
@@ -151,20 +210,62 @@
assertEquals("closed", e.getMessage());
}
try {
- client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
- fail();
- } catch (IllegalStateException e) {
- assertEquals("closed", e.getMessage());
- }
- try {
- client.newMessageSink(TEXT);
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
fail();
} catch (IllegalStateException e) {
assertEquals("closed", e.getMessage());
}
}
- @Test public void serverCloseThenWritingThrows() throws IOException {
+ @Test public void socketClosedDuringPingKillsWebSocket() throws IOException {
+ client2Server.close();
+
+ try {
+ client.sendPing(new Buffer().writeUtf8("Ping!"));
+ fail();
+ } catch (IOException ignored) {
+ }
+
+ // A failed write prevents further use of the WebSocket instance.
+ try {
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
+ }
+ try {
+ client.sendPing(new Buffer().writeUtf8("Ping!"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
+ }
+ }
+
+ @Test public void socketClosedDuringMessageKillsWebSocket() throws IOException {
+ client2Server.close();
+
+ try {
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
+ fail();
+ } catch (IOException ignored) {
+ }
+
+ // A failed write prevents further use of the WebSocket instance.
+ try {
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
+ }
+ try {
+ client.sendPing(new Buffer().writeUtf8("Ping!"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
+ }
+ }
+
+ @Test public void serverCloseThenWritingPingThrows() throws IOException {
server.close(1000, "Hello!");
client.readMessage();
clientListener.assertClose(1000, "Hello!");
@@ -175,12 +276,26 @@
} catch (IOException e) {
assertEquals("closed", e.getMessage());
}
+ }
+
+ @Test public void serverCloseThenWritingMessageThrows() throws IOException {
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
try {
- client.sendMessage(TEXT, new Buffer().writeUtf8("Hi!"));
+ client.sendMessage(RequestBody.create(TEXT, "Hi!"));
fail();
} catch (IOException e) {
assertEquals("closed", e.getMessage());
}
+ }
+
+ @Test public void serverCloseThenWritingCloseThrows() throws IOException {
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
try {
client.close(1000, "Bye!");
fail();
@@ -190,33 +305,34 @@
}
@Test public void serverCloseWhileWritingThrows() throws IOException {
- // Start writing data.
- BufferedSink sink = client.newMessageSink(TEXT);
- sink.writeUtf8("Hel").flush();
+ RequestBody message = new RequestBody() {
+ @Override public MediaType contentType() {
+ return TEXT;
+ }
- server.close(1000, "Hello!");
- client.readMessage();
- clientListener.assertClose(1000, "Hello!");
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ // Start writing data.
+ sink.writeUtf8("Hel").flush();
- try {
- sink.writeUtf8("lo!").emit(); // No writing to the underlying sink.
- fail();
- } catch (IOException e) {
- assertEquals("closed", e.getMessage());
- sink.buffer().clear();
- }
- try {
- sink.flush(); // No flushing.
- fail();
- } catch (IOException e) {
- assertEquals("closed", e.getMessage());
- }
- try {
- sink.close(); // No closing because this requires writing a frame.
- fail();
- } catch (IOException e) {
- assertEquals("closed", e.getMessage());
- }
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
+ try {
+ sink.flush(); // No flushing.
+ fail();
+ } catch (IOException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ try {
+ sink.close(); // No closing because this requires writing a frame.
+ fail();
+ } catch (IOException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ }
+ };
+ client.sendMessage(message);
}
@Test public void clientCloseClosesConnection() throws IOException {
@@ -225,8 +341,7 @@
server.readMessage(); // Read client close, send server close.
serverListener.assertClose(1000, "Hello!");
- client.readMessage(); // Read server close.
- waitForExecutor(clientExecutor); // Close happens asynchronously.
+ client.readMessage(); // Read server close, close connection.
assertTrue(clientConnectionClosed);
clientListener.assertClose(1000, "Hello!");
}
@@ -235,8 +350,8 @@
server.close(1000, "Hello!");
client.readMessage(); // Read server close, send client close, close connection.
- clientListener.assertClose(1000, "Hello!");
assertTrue(clientConnectionClosed);
+ clientListener.assertClose(1000, "Hello!");
server.readMessage();
serverListener.assertClose(1000, "Hello!");
@@ -248,8 +363,7 @@
client.close(1000, "Hi!");
assertFalse(clientConnectionClosed);
- client.readMessage(); // Read close, should NOT send close.
- waitForExecutor(clientExecutor); // Close happens asynchronously.
+ client.readMessage(); // Read close, close connection close.
assertTrue(clientConnectionClosed);
clientListener.assertClose(1000, "Hello!");
@@ -261,7 +375,7 @@
}
@Test public void serverCloseBreaksReadMessageLoop() throws IOException {
- server.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ server.sendMessage(RequestBody.create(TEXT, "Hello!"));
server.close(1000, "Bye!");
assertTrue(client.readMessage());
clientListener.assertTextMessage("Hello!");
@@ -269,12 +383,12 @@
clientListener.assertClose(1000, "Bye!");
}
- @Test public void protocolErrorBeforeCloseSendsClose() {
- server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+ @Test public void protocolErrorBeforeCloseSendsClose() throws IOException {
+ server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
- client.readMessage(); // Detects error, send close.
- clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+ client.readMessage(); // Detects error, send close, close connection.
assertTrue(clientConnectionClosed);
+ clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
server.readMessage();
serverListener.assertClose(1002, "");
@@ -282,14 +396,41 @@
@Test public void protocolErrorAfterCloseDoesNotSendClose() throws IOException {
client.close(1000, "Hello!");
- server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+ assertFalse(clientConnectionClosed); // Not closed until close reply is received.
+ server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
- client.readMessage();
- clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+ client.readMessage(); // Detects error, closes connection immediately since close already sent.
assertTrue(clientConnectionClosed);
+ clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
server.readMessage();
serverListener.assertClose(1000, "Hello!");
+
+ serverListener.assertExhausted(); // Client should not have sent second close.
+ }
+
+ @Test public void closeThrowingClosesConnection() {
+ client2Server.close();
+
+ try {
+ client.close(1000, null);
+ fail();
+ } catch (IOException ignored) {
+ }
+ assertTrue(clientConnectionClosed);
+ }
+
+ @Test public void closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal() throws IOException {
+ client2Server.close();
+ clientConnectionCloseThrows = true;
+
+ try {
+ client.close(1000, "Bye!");
+ fail();
+ } catch (IOException e) {
+ assertNotEquals("Oops!", e.getMessage());
+ }
+ assertTrue(clientConnectionClosed);
}
@Test public void peerConnectionCloseThrowingDoesNotPropagate() throws IOException {
@@ -297,26 +438,72 @@
server.close(1000, "Bye!");
client.readMessage();
- clientListener.assertClose(1000, "Bye!");
assertTrue(clientConnectionClosed);
+ clientListener.assertClose(1000, "Bye!");
server.readMessage();
serverListener.assertClose(1000, "Bye!");
}
- private static void waitForExecutor(Executor executor) {
- final CountDownLatch latch = new CountDownLatch(1);
- executor.execute(new Runnable() {
- @Override public void run() {
- latch.countDown();
- }
- });
- try {
- if (!latch.await(10, TimeUnit.SECONDS)) {
- throw new IllegalStateException("Timed out waiting for executor.");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ static final class MemorySocket implements Closeable {
+ private final Buffer buffer = new Buffer();
+ private boolean closed;
+
+ @Override public void close() {
+ closed = true;
+ }
+
+ Buffer raw() {
+ return buffer;
+ }
+
+ BufferedSource source() {
+ return Okio.buffer(new Source() {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (closed) throw new IOException("closed");
+ return buffer.read(sink, byteCount);
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
+ closed = true;
+ }
+ });
+ }
+
+ BufferedSink sink() {
+ return Okio.buffer(new Sink() {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ if (closed) throw new IOException("closed");
+ buffer.write(source, byteCount);
+ }
+
+ @Override public void flush() throws IOException {
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
+ closed = true;
+ }
+ });
}
}
+
+ static final class SynchronousExecutor implements Executor {
+ @Override public void execute(Runnable command) {
+ command.run();
+ }
+ }
+
+ // ANDROID-BEGIN Android uses JUnit 4.10 which does not have assertNotEquals()
+ private static void assertNotEquals(Object o1, Object o2) {
+ org.junit.Assert.assertFalse(o1 == o2 || (o1 != null && o1.equals(o2)));
+ }
+ // ANDROID-END
}
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
index 1674511..213bda5 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
@@ -15,22 +15,24 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.ws.WebSocketRecorder;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
import org.junit.After;
import org.junit.Test;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
import static com.squareup.okhttp.ws.WebSocketRecorder.MessageDelegate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class WebSocketReaderTest {
@@ -151,11 +153,12 @@
final Buffer sink = new Buffer();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.readFully(sink, 3); // Read "Hel"
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ BufferedSource source = message.source();
+ source.readFully(sink, 3); // Read "Hel"
data.write(ByteString.decodeHex("5158")); // lo
- payload.readFully(sink, 2); // Read "lo"
- payload.close();
+ source.readFully(sink, 2); // Read "lo"
+ source.close();
}
});
serverReader.processNextFrame();
@@ -251,8 +254,8 @@
@Test public void noCloseErrors() throws IOException {
data.write(ByteString.decodeHex("810548656c6c6f")); // Hello
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.readAll(new Buffer());
+ @Override public void onMessage(ResponseBody body) throws IOException {
+ body.source().readAll(new Buffer());
}
});
try {
@@ -269,9 +272,9 @@
final Buffer sink = new Buffer();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.read(sink, 3);
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ message.source().read(sink, 3);
+ message.close();
}
});
@@ -291,9 +294,9 @@
final Buffer sink = new Buffer();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.read(sink, 2);
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ message.source().read(sink, 2);
+ message.close();
}
});
@@ -311,10 +314,10 @@
final AtomicReference<Exception> exception = new AtomicReference<>();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ message.close();
try {
- payload.readAll(new Buffer());
+ message.source().readAll(new Buffer());
fail();
} catch (IllegalStateException e) {
exception.set(e);
@@ -341,7 +344,17 @@
@Test public void emptyCloseCallsCallback() throws IOException {
data.write(ByteString.decodeHex("8800")); // Empty close
clientReader.processNextFrame();
- callback.assertClose(0, "");
+ callback.assertClose(1000, "");
+ }
+
+ @Test public void closeLengthOfOneThrows() throws IOException {
+ data.write(ByteString.decodeHex("880100")); // Close with invalid 1-byte payload
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Malformed close payload length of 1.", e.getMessage());
+ }
}
@Test public void closeCallsCallback() throws IOException {
@@ -350,6 +363,44 @@
callback.assertClose(1000, "Hello");
}
+ @Test public void closeOutOfRangeThrows() throws IOException {
+ data.write(ByteString.decodeHex("88020001")); // Close with code 1
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Code must be in range [1000,5000): 1", e.getMessage());
+ }
+ data.write(ByteString.decodeHex("88021388")); // Close with code 5000
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Code must be in range [1000,5000): 5000", e.getMessage());
+ }
+ }
+
+ @Test public void closeReservedSetThrows() throws IOException {
+ data.write(ByteString.decodeHex("880203ec")); // Close with code 1004
+ data.write(ByteString.decodeHex("880203ed")); // Close with code 1005
+ data.write(ByteString.decodeHex("880203ee")); // Close with code 1006
+ for (int i = 1012; i <= 2999; i++) {
+ data.write(ByteString.decodeHex("8802" + String.format("%04X", i))); // Close with code 'i'
+ }
+
+ int count = 0;
+ for (; !data.exhausted(); count++) {
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ String message = e.getMessage();
+ assertTrue(message, Pattern.matches("Code \\d+ is reserved and may not be used.", message));
+ }
+ }
+ assertEquals(1991, count);
+ }
+
private byte[] binaryData(int length) {
byte[] junk = new byte[length];
random.nextBytes(junk);
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
index a98e6bb..741b33f 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
@@ -17,16 +17,22 @@
import java.io.EOFException;
import java.io.IOException;
-import java.util.Arrays;
import java.util.Random;
import okio.Buffer;
import okio.BufferedSink;
import okio.ByteString;
-import org.junit.After;
+import okio.Okio;
+import okio.Sink;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -35,28 +41,27 @@
private final Buffer data = new Buffer();
private final Random random = new Random(0);
+ /**
+ * Check all data as verified inside of the test. We do this in a rule instead of @After so that
+ * exceptions thrown from the test do not cause this check to fail.
+ */
+ @Rule public final TestRule noDataLeftBehind = new TestRule() {
+ @Override public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override public void evaluate() throws Throwable {
+ base.evaluate();
+ assertEquals("Data not empty", "", data.readByteString().hex());
+ }
+ };
+ }
+ };
+
// Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test.
private final WebSocketWriter serverWriter = new WebSocketWriter(false, data, random);
private final WebSocketWriter clientWriter = new WebSocketWriter(true, data, random);
- @After public void tearDown() throws IOException {
- assertEquals("Data not empty", "", data.readByteString().hex());
- }
-
- @Test public void serverSendSimpleHello() throws IOException {
- Buffer payload = new Buffer().writeUtf8("Hello");
- serverWriter.sendMessage(TEXT, payload);
- assertData("810548656c6c6f");
- }
-
- @Test public void clientSendSimpleHello() throws IOException {
- Buffer payload = new Buffer().writeUtf8("Hello");
- clientWriter.sendMessage(TEXT, payload);
- assertData("818560b420bb28d14cd70f");
- }
-
- @Test public void serverStreamSimpleHello() throws IOException {
- BufferedSink sink = serverWriter.newMessageSink(TEXT);
+ @Test public void serverTextMessage() throws IOException {
+ BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_TEXT));
sink.writeUtf8("Hel").flush();
assertData("010348656c");
@@ -68,19 +73,34 @@
assertData("8000");
}
- @Test public void serverStreamCloseFlushes() throws IOException {
- BufferedSink sink = serverWriter.newMessageSink(TEXT);
+ @Test public void closeFlushes() throws IOException {
+ BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_TEXT));
sink.writeUtf8("Hel").flush();
assertData("010348656c");
sink.writeUtf8("lo").close();
- assertData("00026c6f");
- assertData("8000");
+ assertData("80026c6f");
}
- @Test public void clientStreamSimpleHello() throws IOException {
- BufferedSink sink = clientWriter.newMessageSink(TEXT);
+ @Test public void noWritesAfterClose() throws IOException {
+ Sink sink = serverWriter.newMessageSink(OPCODE_TEXT);
+
+ sink.close();
+ assertData("8100");
+
+ Buffer payload = new Buffer().writeUtf8("Hello");
+ try {
+ // Write to the unbuffered sink as BufferedSink keeps its own closed state.
+ sink.write(payload, payload.size());
+ fail();
+ } catch (IOException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ }
+
+ @Test public void clientTextMessage() throws IOException {
+ BufferedSink sink = Okio.buffer(clientWriter.newMessageSink(OPCODE_TEXT));
sink.writeUtf8("Hel").flush();
assertData("018360b420bb28d14c");
@@ -92,87 +112,84 @@
assertData("80807acb933d");
}
- @Test public void serverSendBinary() throws IOException {
- byte[] payload = binaryData(100);
- serverWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("8264");
- assertData(payload);
- }
+ @Test public void serverBinaryMessage() throws IOException {
+ BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_BINARY));
- @Test public void serverSendBinaryShort() throws IOException {
- byte[] payload = binaryData(0xffff);
- serverWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("827effff");
- assertData(payload);
- }
-
- @Test public void serverSendBinaryLong() throws IOException {
- byte[] payload = binaryData(65537);
- serverWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("827f0000000000010001");
- assertData(payload);
- }
-
- @Test public void clientSendBinary() throws IOException {
- byte[] payload = binaryData(100);
- clientWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("82e4");
-
- byte[] maskKey = new byte[4];
- random.setSeed(0); // Reset the seed so we can mask the payload.
- random.nextBytes(maskKey);
- toggleMask(payload, payload.length, maskKey, 0);
-
- assertData(maskKey);
- assertData(payload);
- }
-
- @Test public void serverStreamBinary() throws IOException {
- byte[] payload = binaryData(100);
- BufferedSink sink = serverWriter.newMessageSink(BINARY);
-
- sink.write(payload, 0, 50).flush();
+ sink.write(binaryData(50)).flush();
assertData("0232");
- assertData(Arrays.copyOfRange(payload, 0, 50));
+ assertData(binaryData(50));
- sink.write(payload, 50, 50).flush();
+ sink.write(binaryData(50)).flush();
assertData("0032");
- assertData(Arrays.copyOfRange(payload, 50, 100));
+ assertData(binaryData(50));
sink.close();
assertData("8000");
}
- @Test public void clientStreamBinary() throws IOException {
+ @Test public void serverMessageLengthShort() throws IOException {
+ Sink sink = serverWriter.newMessageSink(OPCODE_BINARY);
+
+ // Create a payload which will overflow the normal payload byte size.
+ Buffer payload = new Buffer();
+ while (payload.completeSegmentByteCount() <= PAYLOAD_BYTE_MAX) {
+ payload.writeByte('0');
+ }
+ long byteCount = payload.completeSegmentByteCount();
+
+ // Write directly to the unbuffered sink. This ensures it will become single frame.
+ sink.write(payload.clone(), byteCount);
+ assertData("027e"); // 'e' == 4-byte follow-up length.
+ assertData(String.format("%04X", payload.completeSegmentByteCount()));
+ assertData(payload.readByteArray());
+
+ sink.close();
+ assertData("8000");
+ }
+
+ @Test public void serverMessageLengthLong() throws IOException {
+ Sink sink = serverWriter.newMessageSink(OPCODE_BINARY);
+
+ // Create a payload which will overflow the normal and short payload byte size.
+ Buffer payload = new Buffer();
+ while (payload.completeSegmentByteCount() <= PAYLOAD_SHORT_MAX) {
+ payload.writeByte('0');
+ }
+ long byteCount = payload.completeSegmentByteCount();
+
+ // Write directly to the unbuffered sink. This ensures it will become single frame.
+ sink.write(payload.clone(), byteCount);
+ assertData("027f"); // 'f' == 16-byte follow-up length.
+ assertData(String.format("%016X", byteCount));
+ assertData(payload.readByteArray(byteCount));
+
+ sink.close();
+ assertData("8000");
+ }
+
+ @Test public void clientBinary() throws IOException {
byte[] maskKey1 = new byte[4];
random.nextBytes(maskKey1);
byte[] maskKey2 = new byte[4];
random.nextBytes(maskKey2);
- byte[] maskKey3 = new byte[4];
- random.nextBytes(maskKey3);
random.setSeed(0); // Reset the seed so real data matches.
- byte[] payload = binaryData(100);
- BufferedSink sink = clientWriter.newMessageSink(BINARY);
+ BufferedSink sink = Okio.buffer(clientWriter.newMessageSink(OPCODE_BINARY));
- sink.write(payload, 0, 50).flush();
- byte[] part1 = Arrays.copyOfRange(payload, 0, 50);
+ byte[] part1 = binaryData(50);
+ sink.write(part1).flush();
toggleMask(part1, 50, maskKey1, 0);
assertData("02b2");
assertData(maskKey1);
assertData(part1);
- sink.write(payload, 50, 50).flush();
- byte[] part2 = Arrays.copyOfRange(payload, 50, 100);
+ byte[] part2 = binaryData(50);
+ sink.write(part2).close();
toggleMask(part2, 50, maskKey2, 0);
- assertData("00b2");
+ assertData("80b2");
assertData(maskKey2);
assertData(part2);
-
- sink.close();
- assertData("8080");
- assertData(maskKey3);
}
@Test public void serverEmptyClose() throws IOException {
@@ -287,26 +304,16 @@
}
}
- @Test public void twoWritersThrows() {
- clientWriter.newMessageSink(TEXT);
+ @Test public void twoMessageSinksThrows() {
+ clientWriter.newMessageSink(OPCODE_TEXT);
try {
- clientWriter.newMessageSink(TEXT);
+ clientWriter.newMessageSink(OPCODE_TEXT);
fail();
} catch (IllegalStateException e) {
assertEquals("Another message writer is active. Did you call close()?", e.getMessage());
}
}
- @Test public void writeWhileWriterThrows() throws IOException {
- clientWriter.newMessageSink(TEXT);
- try {
- clientWriter.sendMessage(TEXT, new Buffer());
- fail();
- } catch (IllegalStateException e) {
- assertEquals("A message writer is active. Did you call close()?", e.getMessage());
- }
- }
-
private void assertData(String hex) throws EOFException {
ByteString expected = ByteString.decodeHex(hex);
ByteString actual = data.readByteString(expected.size());
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
index 895eb1f..bbc908c 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
@@ -17,7 +17,9 @@
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -30,13 +32,11 @@
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLContext;
import okio.Buffer;
-import okio.BufferedSink;
-import okio.BufferedSource;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
public final class WebSocketCallTest {
@Rule public final MockWebServer server = new MockWebServer();
@@ -64,7 +64,7 @@
server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = awaitWebSocket();
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+ webSocket.sendMessage(RequestBody.create(TEXT, "Hello, WebSockets!"));
serverListener.assertTextMessage("Hello, WebSockets!");
}
@@ -74,43 +74,7 @@
new Thread() {
@Override public void run() {
try {
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
- } catch (IOException e) {
- throw new AssertionError(e);
- }
- }
- }.start();
- }
- };
- server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
-
- awaitWebSocket();
- listener.assertTextMessage("Hello, WebSockets!");
- }
-
- @Test public void clientStreamingMessage() throws IOException {
- WebSocketRecorder serverListener = new WebSocketRecorder();
- server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
-
- WebSocket webSocket = awaitWebSocket();
- BufferedSink sink = webSocket.newMessageSink(TEXT);
- sink.writeUtf8("Hello, ").flush();
- sink.writeUtf8("WebSockets!").flush();
- sink.close();
-
- serverListener.assertTextMessage("Hello, WebSockets!");
- }
-
- @Test public void serverStreamingMessage() throws IOException {
- WebSocketListener serverListener = new EmptyWebSocketListener() {
- @Override public void onOpen(final WebSocket webSocket, Response response) {
- new Thread() {
- @Override public void run() {
- try {
- BufferedSink sink = webSocket.newMessageSink(TEXT);
- sink.writeUtf8("Hello, ").flush();
- sink.writeUtf8("WebSockets!").flush();
- sink.close();
+ webSocket.sendMessage(RequestBody.create(TEXT, "Hello, WebSockets!"));
} catch (IOException e) {
throw new AssertionError(e);
}
@@ -233,7 +197,7 @@
.build();
WebSocket webSocket = awaitWebSocket(request1);
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("abc"));
+ webSocket.sendMessage(RequestBody.create(TEXT, "abc"));
serverListener.assertTextMessage("abc");
}
@@ -255,9 +219,8 @@
latch.countDown();
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
- listener.onMessage(payload, type);
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ listener.onMessage(message);
}
@Override public void onPong(Buffer payload) {
@@ -290,8 +253,7 @@
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
+ @Override public void onMessage(ResponseBody message) throws IOException {
}
@Override public void onPong(Buffer payload) {
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
index 56b3810..3e82e05 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
@@ -15,24 +15,25 @@
*/
package com.squareup.okhttp.ws;
+import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.ws.WebSocketReader;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import okio.Buffer;
-import okio.BufferedSource;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public final class WebSocketRecorder implements WebSocketReader.FrameCallback, WebSocketListener {
public interface MessageDelegate {
- void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException;
+ void onMessage(ResponseBody message) throws IOException;
}
private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
@@ -46,16 +47,15 @@
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource source, WebSocket.PayloadType type)
- throws IOException {
+ @Override public void onMessage(ResponseBody message) throws IOException {
if (delegate != null) {
- delegate.onMessage(source, type);
+ delegate.onMessage(message);
delegate = null;
} else {
- Message message = new Message(type);
- source.readAll(message.buffer);
- source.close();
- events.add(message);
+ Message event = new Message(message.contentType());
+ message.source().readAll(event.buffer);
+ message.close();
+ events.add(event);
}
}
@@ -87,28 +87,48 @@
}
}
- public void assertTextMessage(String payload) {
+ public void assertTextMessage(String payload) throws IOException {
Message message = new Message(TEXT);
message.buffer.writeUtf8(payload);
- assertEquals(message, nextEvent());
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(message, actual);
}
- public void assertBinaryMessage(byte[] payload) {
+ public void assertBinaryMessage(byte[] payload) throws IOException {
Message message = new Message(BINARY);
message.buffer.write(payload);
- assertEquals(message, nextEvent());
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(message, actual);
}
- public void assertPing(Buffer payload) {
- assertEquals(new Ping(payload), nextEvent());
+ public void assertPing(Buffer payload) throws IOException {
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(new Ping(payload), actual);
}
- public void assertPong(Buffer payload) {
- assertEquals(new Pong(payload), nextEvent());
+ public void assertPong(Buffer payload) throws IOException {
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(new Pong(payload), actual);
}
- public void assertClose(int code, String reason) {
- assertEquals(new Close(code, reason), nextEvent());
+ public void assertClose(int code, String reason) throws IOException {
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(new Close(code, reason), actual);
}
public void assertFailure(Class<? extends IOException> cls, String message) {
@@ -125,25 +145,25 @@
}
private static class Message {
- public final WebSocket.PayloadType type;
+ public final MediaType mediaType;
public final Buffer buffer = new Buffer();
- private Message(WebSocket.PayloadType type) {
- this.type = type;
+ private Message(MediaType mediaType) {
+ this.mediaType = mediaType;
}
@Override public String toString() {
- return "Message[" + type + " " + buffer + "]";
+ return "Message[" + mediaType + " " + buffer + "]";
}
@Override public int hashCode() {
- return type.hashCode() * 37 + buffer.hashCode();
+ return mediaType.hashCode() * 37 + buffer.hashCode();
}
@Override public boolean equals(Object obj) {
if (obj instanceof Message) {
Message other = (Message) obj;
- return type == other.type && buffer.equals(other.buffer);
+ return mediaType.equals(other.mediaType) && buffer.equals(other.buffer);
}
return false;
}
diff --git a/okhttp-ws/pom.xml b/okhttp-ws/pom.xml
index 81f8afd..688b538 100644
--- a/okhttp-ws/pom.xml
+++ b/okhttp-ws/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-ws</artifactId>
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
index 8d6b7c4..ea55b5a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
@@ -15,6 +15,9 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.NamedRunnable;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketListener;
@@ -22,14 +25,17 @@
import java.net.ProtocolException;
import java.util.Random;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
+import okio.Okio;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
import static com.squareup.okhttp.internal.ws.WebSocketReader.FrameCallback;
public abstract class RealWebSocket implements WebSocket {
- /** A close code which indicates that the peer encountered a protocol exception. */
private static final int CLOSE_PROTOCOL_EXCEPTION = 1002;
private final WebSocketWriter writer;
@@ -38,10 +44,13 @@
/** True after calling {@link #close(int, String)}. No writes are allowed afterward. */
private volatile boolean writerSentClose;
+ /** True after {@link IOException}. {@link #close(int, String)} becomes only valid call. */
+ private boolean writerWantsClose;
/** True after a close frame was read by the reader. No frames will follow it. */
- private volatile boolean readerSentClose;
- /** Lock required to negotiate closing the connection. */
- private final Object closeLock = new Object();
+ private boolean readerSentClose;
+
+ /** True after calling {@link #close()} to free connection resources. */
+ private final AtomicBoolean connectionClosed = new AtomicBoolean();
public RealWebSocket(boolean isClient, BufferedSource source, BufferedSink sink, Random random,
final Executor replyExecutor, final WebSocketListener listener, final String url) {
@@ -49,8 +58,8 @@
writer = new WebSocketWriter(isClient, sink, random);
reader = new WebSocketReader(isClient, source, new FrameCallback() {
- @Override public void onMessage(BufferedSource source, PayloadType type) throws IOException {
- listener.onMessage(source, type);
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ listener.onMessage(message);
}
@Override public void onPing(final Buffer buffer) {
@@ -69,17 +78,10 @@
}
@Override public void onClose(final int code, final String reason) {
- final boolean writeCloseResponse;
- synchronized (closeLock) {
- readerSentClose = true;
-
- // If the writer has not indicated a desire to close we will write a close response.
- writeCloseResponse = !writerSentClose;
- }
-
+ readerSentClose = true;
replyExecutor.execute(new NamedRunnable("OkHttp %s WebSocket Close Reply", url) {
@Override protected void execute() {
- peerClose(code, reason, writeCloseResponse);
+ peerClose(code, reason);
}
});
}
@@ -100,57 +102,96 @@
}
}
- @Override public BufferedSink newMessageSink(PayloadType type) {
+ @Override public void sendMessage(RequestBody message) throws IOException {
+ if (message == null) throw new NullPointerException("message == null");
if (writerSentClose) throw new IllegalStateException("closed");
- return writer.newMessageSink(type);
- }
+ if (writerWantsClose) throw new IllegalStateException("must call close()");
- @Override public void sendMessage(PayloadType type, Buffer payload) throws IOException {
- if (writerSentClose) throw new IllegalStateException("closed");
- writer.sendMessage(type, payload);
+ MediaType contentType = message.contentType();
+ if (contentType == null) {
+ throw new IllegalArgumentException(
+ "Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.");
+ }
+ String contentSubtype = contentType.subtype();
+
+ int formatOpcode;
+ if (WebSocket.TEXT.subtype().equals(contentSubtype)) {
+ formatOpcode = OPCODE_TEXT;
+ } else if (WebSocket.BINARY.subtype().equals(contentSubtype)) {
+ formatOpcode = OPCODE_BINARY;
+ } else {
+ throw new IllegalArgumentException("Unknown message content type: "
+ + contentType.type() + "/" + contentType.subtype() // Omit any implicitly added charset.
+ + ". Must use WebSocket.TEXT or WebSocket.BINARY.");
+ }
+
+ BufferedSink sink = Okio.buffer(writer.newMessageSink(formatOpcode));
+ try {
+ message.writeTo(sink);
+ sink.close();
+ } catch (IOException e) {
+ writerWantsClose = true;
+ throw e;
+ }
}
@Override public void sendPing(Buffer payload) throws IOException {
if (writerSentClose) throw new IllegalStateException("closed");
- writer.writePing(payload);
+ if (writerWantsClose) throw new IllegalStateException("must call close()");
+
+ try {
+ writer.writePing(payload);
+ } catch (IOException e) {
+ writerWantsClose = true;
+ throw e;
+ }
}
/** Send an unsolicited pong with the specified payload. */
public void sendPong(Buffer payload) throws IOException {
if (writerSentClose) throw new IllegalStateException("closed");
- writer.writePong(payload);
+ if (writerWantsClose) throw new IllegalStateException("must call close()");
+
+ try {
+ writer.writePong(payload);
+ } catch (IOException e) {
+ writerWantsClose = true;
+ throw e;
+ }
}
@Override public void close(int code, String reason) throws IOException {
if (writerSentClose) throw new IllegalStateException("closed");
+ writerSentClose = true;
- boolean closeConnection;
- synchronized (closeLock) {
- writerSentClose = true;
-
- // If the reader has also indicated a desire to close we will close the connection.
- closeConnection = readerSentClose;
- }
-
- writer.writeClose(code, reason);
-
- if (closeConnection) {
- closeConnection();
+ try {
+ writer.writeClose(code, reason);
+ } catch (IOException e) {
+ if (connectionClosed.compareAndSet(false, true)) {
+ // Try to close the connection without masking the original exception.
+ try {
+ close();
+ } catch (IOException ignored) {
+ }
+ }
+ throw e;
}
}
/** Replies and closes this web socket when a close frame is read from the peer. */
- private void peerClose(int code, String reason, boolean writeCloseResponse) {
- if (writeCloseResponse) {
+ private void peerClose(int code, String reason) {
+ if (!writerSentClose) {
try {
writer.writeClose(code, reason);
} catch (IOException ignored) {
}
}
- try {
- closeConnection();
- } catch (IOException ignored) {
+ if (connectionClosed.compareAndSet(false, true)) {
+ try {
+ close();
+ } catch (IOException ignored) {
+ }
}
listener.onClose(code, reason);
@@ -158,32 +199,24 @@
/** Called on the reader thread when an error occurs. */
private void readerErrorClose(IOException e) {
- boolean writeCloseResponse;
- synchronized (closeLock) {
- readerSentClose = true;
-
- // If the writer has not closed we will close the connection.
- writeCloseResponse = !writerSentClose;
- }
-
- if (writeCloseResponse) {
- if (e instanceof ProtocolException) {
- // For protocol exceptions, try to inform the server of such.
- try {
- writer.writeClose(CLOSE_PROTOCOL_EXCEPTION, null);
- } catch (IOException ignored) {
- }
+ // For protocol exceptions, try to inform the server of such.
+ if (!writerSentClose && e instanceof ProtocolException) {
+ try {
+ writer.writeClose(CLOSE_PROTOCOL_EXCEPTION, null);
+ } catch (IOException ignored) {
}
}
- try {
- closeConnection();
- } catch (IOException ignored) {
+ if (connectionClosed.compareAndSet(false, true)) {
+ try {
+ close();
+ } catch (IOException ignored) {
+ }
}
listener.onFailure(e, null);
}
- /** Perform any tear-down work on the connection (close the socket, recycle, etc.). */
- protected abstract void closeConnection() throws IOException;
+ /** Perform any tear-down work (close the connection, shutdown executors). */
+ protected abstract void close() throws IOException;
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
index 2b93398..0778278 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
@@ -68,14 +68,16 @@
static final int OPCODE_CONTROL_PONG = 0xa;
/**
- * Maximum length of frame payload. Larger payloads, if supported, can use the special values
- * {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}.
+ * Maximum length of frame payload. Larger payloads, if supported by the frame type, can use the
+ * special values {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}.
*/
- static final int PAYLOAD_MAX = 125;
+ static final long PAYLOAD_BYTE_MAX = 125L;
/**
* Value for {@link #B1_MASK_LENGTH} which indicates the next two bytes are the unsigned length.
*/
static final int PAYLOAD_SHORT = 126;
+ /** Maximum length of a frame payload to be denoted as {@link #PAYLOAD_SHORT}. */
+ static final long PAYLOAD_SHORT_MAX = 0xffffL;
/**
* Value for {@link #B1_MASK_LENGTH} which indicates the next eight bytes are the unsigned
* length.
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
index ce548b1..d81785a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
@@ -15,6 +15,9 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.ws.WebSocket;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
@@ -24,7 +27,6 @@
import okio.Source;
import okio.Timeout;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_FIN;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV1;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV2;
@@ -40,7 +42,7 @@
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_FLAG_CONTROL;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_LONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
import static java.lang.Integer.toHexString;
@@ -50,7 +52,7 @@
*/
public final class WebSocketReader {
public interface FrameCallback {
- void onMessage(BufferedSource source, PayloadType type) throws IOException;
+ void onMessage(ResponseBody body) throws IOException;
void onPing(Buffer buffer);
void onPong(Buffer buffer);
void onClose(int code, String reason);
@@ -145,8 +147,8 @@
}
frameBytesRead = 0;
- if (isControlFrame && frameLength > PAYLOAD_MAX) {
- throw new ProtocolException("Control frame must be less than " + PAYLOAD_MAX + "B.");
+ if (isControlFrame && frameLength > PAYLOAD_BYTE_MAX) {
+ throw new ProtocolException("Control frame must be less than " + PAYLOAD_BYTE_MAX + "B.");
}
if (isMasked) {
@@ -182,18 +184,23 @@
frameCallback.onPong(buffer);
break;
case OPCODE_CONTROL_CLOSE:
- int code = 0;
+ int code = 1000;
String reason = "";
if (buffer != null) {
- if (buffer.size() < 2) {
- throw new ProtocolException("Close payload must be at least two bytes.");
- }
- code = buffer.readShort();
- if (code < 1000 || code >= 5000) {
- throw new ProtocolException("Code must be in range [1000,5000): " + code);
- }
+ long bufferSize = buffer.size();
+ if (bufferSize == 1) {
+ throw new ProtocolException("Malformed close payload length of 1.");
+ } else if (bufferSize != 0) {
+ code = buffer.readShort();
+ if (code < 1000 || code >= 5000) {
+ throw new ProtocolException("Code must be in range [1000,5000): " + code);
+ }
+ if ((code >= 1004 && code <= 1006) || (code >= 1012 && code <= 2999)) {
+ throw new ProtocolException("Code " + code + " is reserved and may not be used.");
+ }
- reason = buffer.readUtf8();
+ reason = buffer.readUtf8();
+ }
}
frameCallback.onClose(code, reason);
closed = true;
@@ -204,20 +211,35 @@
}
private void readMessageFrame() throws IOException {
- PayloadType type;
+ final MediaType type;
switch (opcode) {
case OPCODE_TEXT:
- type = PayloadType.TEXT;
+ type = WebSocket.TEXT;
break;
case OPCODE_BINARY:
- type = PayloadType.BINARY;
+ type = WebSocket.BINARY;
break;
default:
throw new ProtocolException("Unknown opcode: " + toHexString(opcode));
}
+ final BufferedSource source = Okio.buffer(framedMessageSource);
+ ResponseBody body = new ResponseBody() {
+ @Override public MediaType contentType() {
+ return type;
+ }
+
+ @Override public long contentLength() throws IOException {
+ return -1;
+ }
+
+ @Override public BufferedSource source() throws IOException {
+ return source;
+ }
+ };
+
messageClosed = false;
- frameCallback.onMessage(Okio.buffer(framedMessageSource), type);
+ frameCallback.onMessage(body);
if (!messageClosed) {
throw new IllegalStateException("Listener failed to call close on message payload.");
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
index fc5de75..feece7a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
@@ -20,42 +20,41 @@
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
-import okio.Okio;
import okio.Sink;
import okio.Timeout;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_FIN;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B1_FLAG_MASK;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTINUATION;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_CLOSE;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PING;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_LONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
/**
* An <a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a>-compatible WebSocket frame writer.
* <p>
* This class is partially thread safe. Only a single "main" thread should be sending messages via
- * calls to {@link #newMessageSink} or {@link #sendMessage} as well as any calls to
- * {@link #writePing} or {@link #writeClose}. Other threads may call {@link #writePing},
- * {@link #writePong}, or {@link #writeClose} which will interleave on the wire with frames from
- * the main thread.
+ * calls to {@link #newMessageSink}, {@link #writePing}, or {@link #writeClose}. Other threads may
+ * call {@link #writePing}, {@link #writePong}, or {@link #writeClose} which will interleave on the
+ * wire with frames from the "main" sending thread.
*/
public final class WebSocketWriter {
private final boolean isClient;
- /** Writes must be guarded by synchronizing on this instance! */
- private final BufferedSink sink;
private final Random random;
+ /** Writes must be guarded by synchronizing on 'this'. */
+ private final BufferedSink sink;
+ /** Access must be guarded by synchronizing on 'this'. */
+ private boolean writerClosed;
+
+ private final Buffer buffer = new Buffer();
private final FrameSink frameSink = new FrameSink();
- private boolean closed;
private boolean activeWriter;
private final byte[] maskKey;
@@ -75,15 +74,15 @@
/** Send a ping with the supplied {@code payload}. Payload may be {@code null} */
public void writePing(Buffer payload) throws IOException {
- synchronized (sink) {
- writeControlFrame(OPCODE_CONTROL_PING, payload);
+ synchronized (this) {
+ writeControlFrameSynchronized(OPCODE_CONTROL_PING, payload);
}
}
/** Send a pong with the supplied {@code payload}. Payload may be {@code null} */
public void writePong(Buffer payload) throws IOException {
- synchronized (sink) {
- writeControlFrame(OPCODE_CONTROL_PONG, payload);
+ synchronized (this) {
+ writeControlFrameSynchronized(OPCODE_CONTROL_PONG, payload);
}
}
@@ -108,21 +107,23 @@
}
}
- synchronized (sink) {
- writeControlFrame(OPCODE_CONTROL_CLOSE, payload);
- closed = true;
+ synchronized (this) {
+ writeControlFrameSynchronized(OPCODE_CONTROL_CLOSE, payload);
+ writerClosed = true;
}
}
- private void writeControlFrame(int opcode, Buffer payload) throws IOException {
- if (closed) throw new IOException("closed");
+ private void writeControlFrameSynchronized(int opcode, Buffer payload) throws IOException {
+ assert Thread.holdsLock(this);
+
+ if (writerClosed) throw new IOException("closed");
int length = 0;
if (payload != null) {
length = (int) payload.size();
- if (length > PAYLOAD_MAX) {
+ if (length > PAYLOAD_BYTE_MAX) {
throw new IllegalArgumentException(
- "Payload size must be less than or equal to " + PAYLOAD_MAX);
+ "Payload size must be less than or equal to " + PAYLOAD_BYTE_MAX);
}
}
@@ -138,7 +139,7 @@
sink.write(maskKey);
if (payload != null) {
- writeAllMasked(payload, length);
+ writeMaskedSynchronized(payload, length);
}
} else {
sink.writeByte(b1);
@@ -148,93 +149,70 @@
}
}
- sink.flush();
+ sink.emit();
}
/**
* Stream a message payload as a series of frames. This allows control frames to be interleaved
* between parts of the message.
*/
- public BufferedSink newMessageSink(PayloadType type) {
- if (type == null) throw new NullPointerException("type == null");
+ public Sink newMessageSink(int formatOpcode) {
if (activeWriter) {
throw new IllegalStateException("Another message writer is active. Did you call close()?");
}
activeWriter = true;
- frameSink.payloadType = type;
+ // Reset FrameSink state for a new writer.
+ frameSink.formatOpcode = formatOpcode;
frameSink.isFirstFrame = true;
- return Okio.buffer(frameSink);
+ frameSink.closed = false;
+
+ return frameSink;
}
- /**
- * Send a message payload as a single frame. This will block any control frames that need sent
- * until it is completed.
- */
- public void sendMessage(PayloadType type, Buffer payload) throws IOException {
- if (type == null) throw new NullPointerException("type == null");
- if (payload == null) throw new NullPointerException("payload == null");
- if (activeWriter) {
- throw new IllegalStateException("A message writer is active. Did you call close()?");
+ private void writeMessageFrameSynchronized(int formatOpcode, long byteCount, boolean isFirstFrame,
+ boolean isFinal) throws IOException {
+ assert Thread.holdsLock(this);
+
+ if (writerClosed) throw new IOException("closed");
+
+ int b0 = isFirstFrame ? formatOpcode : OPCODE_CONTINUATION;
+ if (isFinal) {
+ b0 |= B0_FLAG_FIN;
}
- writeFrame(type, payload, payload.size(), true /* first frame */, true /* final */);
+ sink.writeByte(b0);
+
+ int b1 = 0;
+ if (isClient) {
+ b1 |= B1_FLAG_MASK;
+ random.nextBytes(maskKey);
+ }
+ if (byteCount <= PAYLOAD_BYTE_MAX) {
+ b1 |= (int) byteCount;
+ sink.writeByte(b1);
+ } else if (byteCount <= PAYLOAD_SHORT_MAX) {
+ b1 |= PAYLOAD_SHORT;
+ sink.writeByte(b1);
+ sink.writeShort((int) byteCount);
+ } else {
+ b1 |= PAYLOAD_LONG;
+ sink.writeByte(b1);
+ sink.writeLong(byteCount);
+ }
+
+ if (isClient) {
+ sink.write(maskKey);
+ writeMaskedSynchronized(buffer, byteCount);
+ } else {
+ sink.write(buffer, byteCount);
+ }
+
+ sink.emit();
}
- private void writeFrame(PayloadType payloadType, Buffer source, long byteCount,
- boolean isFirstFrame, boolean isFinal) throws IOException {
- if (closed) throw new IOException("closed");
+ private void writeMaskedSynchronized(BufferedSource source, long byteCount) throws IOException {
+ assert Thread.holdsLock(this);
- int opcode = OPCODE_CONTINUATION;
- if (isFirstFrame) {
- switch (payloadType) {
- case TEXT:
- opcode = OPCODE_TEXT;
- break;
- case BINARY:
- opcode = OPCODE_BINARY;
- break;
- default:
- throw new IllegalStateException("Unknown payload type: " + payloadType);
- }
- }
-
- synchronized (sink) {
- int b0 = opcode;
- if (isFinal) {
- b0 |= B0_FLAG_FIN;
- }
- sink.writeByte(b0);
-
- int b1 = 0;
- if (isClient) {
- b1 |= B1_FLAG_MASK;
- random.nextBytes(maskKey);
- }
- if (byteCount <= PAYLOAD_MAX) {
- b1 |= (int) byteCount;
- sink.writeByte(b1);
- } else if (byteCount <= 0xffffL) { // Unsigned short.
- b1 |= PAYLOAD_SHORT;
- sink.writeByte(b1);
- sink.writeShort((int) byteCount);
- } else {
- b1 |= PAYLOAD_LONG;
- sink.writeByte(b1);
- sink.writeLong(byteCount);
- }
-
- if (isClient) {
- sink.write(maskKey);
- writeAllMasked(source, byteCount);
- } else {
- sink.write(source, byteCount);
- }
-
- sink.flush();
- }
- }
-
- private void writeAllMasked(BufferedSource source, long byteCount) throws IOException {
long written = 0;
while (written < byteCount) {
int toRead = (int) Math.min(byteCount, maskBuffer.length);
@@ -247,20 +225,31 @@
}
private final class FrameSink implements Sink {
- private PayloadType payloadType;
+ private int formatOpcode;
private boolean isFirstFrame;
+ private boolean closed;
@Override public void write(Buffer source, long byteCount) throws IOException {
- writeFrame(payloadType, source, byteCount, isFirstFrame, false /* final */);
- isFirstFrame = false;
+ if (closed) throw new IOException("closed");
+
+ buffer.write(source, byteCount);
+
+ long emitCount = buffer.completeSegmentByteCount();
+ if (emitCount > 0) {
+ synchronized (WebSocketWriter.this) {
+ writeMessageFrameSynchronized(formatOpcode, emitCount, isFirstFrame, false /* final */);
+ }
+ isFirstFrame = false;
+ }
}
@Override public void flush() throws IOException {
if (closed) throw new IOException("closed");
- synchronized (sink) {
- sink.flush();
+ synchronized (WebSocketWriter.this) {
+ writeMessageFrameSynchronized(formatOpcode, buffer.size(), isFirstFrame, false /* final */);
}
+ isFirstFrame = false;
}
@Override public Timeout timeout() {
@@ -271,21 +260,10 @@
@Override public void close() throws IOException {
if (closed) throw new IOException("closed");
- int length = 0;
-
- synchronized (sink) {
- sink.writeByte(B0_FLAG_FIN | OPCODE_CONTINUATION);
-
- if (isClient) {
- sink.writeByte(B1_FLAG_MASK | length);
- random.nextBytes(maskKey);
- sink.write(maskKey);
- } else {
- sink.writeByte(length);
- }
- sink.flush();
+ synchronized (WebSocketWriter.this) {
+ writeMessageFrameSynchronized(formatOpcode, buffer.size(), isFirstFrame, true /* final */);
}
-
+ closed = true;
activeWriter = false;
}
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
index 4cf2f42..a3eebe7 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
@@ -15,40 +15,35 @@
*/
package com.squareup.okhttp.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
import java.io.IOException;
import okio.Buffer;
-import okio.BufferedSink;
/** Blocking interface to connect and write to a web socket. */
public interface WebSocket {
- /** The format of a message payload. */
- enum PayloadType {
- /** UTF8-encoded text data. */
- TEXT,
- /** Arbitrary binary data. */
- BINARY
- }
+ /** A {@link MediaType} indicating UTF-8 text frames should be used when sending the message. */
+ MediaType TEXT = MediaType.parse("application/vnd.okhttp.websocket+text; charset=utf-8");
+ /** A {@link MediaType} indicating binary frames should be used when sending the message. */
+ MediaType BINARY = MediaType.parse("application/vnd.okhttp.websocket+binary");
/**
- * Stream a message payload to the server of the specified {code type}.
- * <p>
- * You must call {@link BufferedSink#close() close()} to complete the message. Calls to
- * {@link BufferedSink#flush() flush()} write a frame fragment. The message may be empty.
+ * Send a message payload to the server.
*
+ * <p>The {@linkplain RequestBody#contentType() content type} of {@code message} should be either
+ * {@link #TEXT} or {@link #BINARY}.
+ *
+ * @throws IOException if unable to write the message. Clients must call {@link #close} when this
+ * happens to ensure resources are cleaned up.
* @throws IllegalStateException if not connected, already closed, or another writer is active.
*/
- BufferedSink newMessageSink(WebSocket.PayloadType type);
-
- /**
- * Send a message payload to the server of the specified {@code type}.
- *
- * @throws IllegalStateException if not connected, already closed, or another writer is active.
- */
- void sendMessage(WebSocket.PayloadType type, Buffer payload) throws IOException;
+ void sendMessage(RequestBody message) throws IOException;
/**
* Send a ping to the server with optional payload.
*
+ * @throws IOException if unable to write the ping. Clients must call {@link #close} when this
+ * happens to ensure resources are cleaned up.
* @throws IllegalStateException if already closed.
*/
void sendPing(Buffer payload) throws IOException;
@@ -62,6 +57,7 @@
* It is an error to call this method before calling close on an active writer. Calling this
* method more than once has no effect.
*
+ * @throws IOException if unable to write the close message. Resources will still be freed.
* @throws IllegalStateException if already closed.
*/
void close(int code, String reason) throws IOException;
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
index 46ee8a1..5950850 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
@@ -17,12 +17,12 @@
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Callback;
-import com.squareup.okhttp.Connection;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.StreamAllocation;
import com.squareup.okhttp.internal.ws.RealWebSocket;
import com.squareup.okhttp.internal.ws.WebSocketProtocol;
import java.io.IOException;
@@ -30,11 +30,9 @@
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Random;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
-import okio.BufferedSink;
-import okio.BufferedSource;
import okio.ByteString;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -47,7 +45,6 @@
return new WebSocketCall(client, request);
}
- private final Request request;
private final Call call;
private final Random random;
private final String key;
@@ -78,7 +75,6 @@
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.build();
- this.request = request;
call = client.newCall(request);
}
@@ -118,11 +114,9 @@
call.cancel();
}
- private void createWebSocket(Response response, WebSocketListener listener)
- throws IOException {
+ private void createWebSocket(Response response, WebSocketListener listener) throws IOException {
if (response.code() != 101) {
- // TODO call.engine.releaseConnection();
- Internal.instance.callEngineReleaseConnection(call);
+ Util.closeQuietly(response.body());
throw new ProtocolException("Expected HTTP 101 response but was '"
+ response.code()
+ " "
@@ -150,21 +144,9 @@
+ "'");
}
- // TODO connection = call.engine.getConnection();
- Connection connection = Internal.instance.callEngineGetConnection(call);
- // TODO if (!connection.clearOwner()) {
- if (!Internal.instance.clearOwner(connection)) {
- throw new IllegalStateException("Unable to take ownership of connection.");
- }
-
- BufferedSource source = Internal.instance.connectionRawSource(connection);
- BufferedSink sink = Internal.instance.connectionRawSink(connection);
-
- final RealWebSocket webSocket =
- ConnectionWebSocket.create(response, connection, source, sink, random, listener);
-
- // TODO connection.setOwner(webSocket);
- Internal.instance.connectionSetOwner(connection, webSocket);
+ StreamAllocation streamAllocation = Internal.instance.callEngineGetStreamAllocation(call);
+ RealWebSocket webSocket = StreamWebSocket.create(
+ streamAllocation, response, random, listener);
listener.onOpen(webSocket, response);
@@ -173,30 +155,33 @@
}
// Keep static so that the WebSocketCall instance can be garbage collected.
- private static class ConnectionWebSocket extends RealWebSocket {
- static RealWebSocket create(Response response, Connection connection, BufferedSource source,
- BufferedSink sink, Random random, WebSocketListener listener) {
+ private static class StreamWebSocket extends RealWebSocket {
+ static RealWebSocket create(StreamAllocation streamAllocation, Response response,
+ Random random, WebSocketListener listener) {
String url = response.request().urlString();
ThreadPoolExecutor replyExecutor =
new ThreadPoolExecutor(1, 1, 1, SECONDS, new LinkedBlockingDeque<Runnable>(),
Util.threadFactory(String.format("OkHttp %s WebSocket", url), true));
replyExecutor.allowCoreThreadTimeOut(true);
- return new ConnectionWebSocket(connection, source, sink, random, replyExecutor, listener,
- url);
+ return new StreamWebSocket(streamAllocation, random, replyExecutor, listener, url);
}
- private final Connection connection;
+ private final StreamAllocation streamAllocation;
+ private final ExecutorService replyExecutor;
- private ConnectionWebSocket(Connection connection, BufferedSource source, BufferedSink sink,
- Random random, Executor replyExecutor, WebSocketListener listener, String url) {
- super(true /* is client */, source, sink, random, replyExecutor, listener, url);
- this.connection = connection;
+ private StreamWebSocket(StreamAllocation streamAllocation,
+ Random random, ExecutorService replyExecutor, WebSocketListener listener, String url) {
+ super(true /* is client */, streamAllocation.connection().source,
+ streamAllocation.connection().sink, random, replyExecutor, listener, url);
+ this.streamAllocation = streamAllocation;
+ this.replyExecutor = replyExecutor;
}
- @Override protected void closeConnection() throws IOException {
- // TODO connection.closeIfOwnedBy(this);
- Internal.instance.closeIfOwnedBy(connection, this);
+ @Override protected void close() throws IOException {
+ replyExecutor.shutdown();
+ streamAllocation.noNewStreams();
+ streamAllocation.streamFinished(streamAllocation.stream());
}
}
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
index 8941b74..5a5a8b1 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
@@ -16,11 +16,9 @@
package com.squareup.okhttp.ws;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
import okio.Buffer;
-import okio.BufferedSource;
-
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
/** Listener for server-initiated messages on a connected {@link WebSocket}. */
public interface WebSocketListener {
@@ -50,8 +48,11 @@
* <p>Implementations <strong>must</strong> call {@code source.close()} before returning. This
* indicates completion of parsing the message payload and will consume any remaining bytes in
* the message.
+ *
+ * <p>The {@linkplain ResponseBody#contentType() content type} of {@code message} will be either
+ * {@link WebSocket#TEXT} or {@link WebSocket#BINARY} which indicates the format of the message.
*/
- void onMessage(BufferedSource payload, PayloadType type) throws IOException;
+ void onMessage(ResponseBody message) throws IOException;
/**
* Called when a server pong is received. This is usually a result of calling {@link
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index 5cd1187..3254b30 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp</artifactId>
@@ -17,6 +17,11 @@
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.google.android</groupId>
+ <artifactId>android</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Address.java b/okhttp/src/main/java/com/squareup/okhttp/Address.java
index cd3687b..9efdf28 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Address.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Address.java
@@ -30,57 +30,90 @@
* this is the server's hostname and port. If an explicit proxy is requested (or
* {@linkplain Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
* that proxy information. For secure connections the address also includes the
- * SSL socket factory and hostname verifier.
+ * SSL socket factory, hostname verifier, and certificate pinner.
*
* <p>HTTP requests that share the same {@code Address} may also share the same
* {@link Connection}.
*/
public final class Address {
- final Proxy proxy;
- final String uriHost;
- final int uriPort;
+ final HttpUrl url;
+ final Dns dns;
final SocketFactory socketFactory;
- final SSLSocketFactory sslSocketFactory;
- final HostnameVerifier hostnameVerifier;
- final CertificatePinner certificatePinner;
final Authenticator authenticator;
final List<Protocol> protocols;
final List<ConnectionSpec> connectionSpecs;
final ProxySelector proxySelector;
+ final Proxy proxy;
+ final SSLSocketFactory sslSocketFactory;
+ final HostnameVerifier hostnameVerifier;
+ final CertificatePinner certificatePinner;
- public Address(String uriHost, int uriPort, SocketFactory socketFactory,
+ public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,
SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,
CertificatePinner certificatePinner, Authenticator authenticator, Proxy proxy,
List<Protocol> protocols, List<ConnectionSpec> connectionSpecs, ProxySelector proxySelector) {
- if (uriHost == null) throw new NullPointerException("uriHost == null");
- if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
- if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
- if (protocols == null) throw new IllegalArgumentException("protocols == null");
- if (proxySelector == null) throw new IllegalArgumentException("proxySelector == null");
- this.proxy = proxy;
- this.uriHost = uriHost;
- this.uriPort = uriPort;
+ this.url = new HttpUrl.Builder()
+ .scheme(sslSocketFactory != null ? "https" : "http")
+ .host(uriHost)
+ .port(uriPort)
+ .build();
+
+ if (dns == null) throw new IllegalArgumentException("dns == null");
+ this.dns = dns;
+
+ if (socketFactory == null) throw new IllegalArgumentException("socketFactory == null");
this.socketFactory = socketFactory;
+
+ if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
+ this.authenticator = authenticator;
+
+ if (protocols == null) throw new IllegalArgumentException("protocols == null");
+ this.protocols = Util.immutableList(protocols);
+
+ if (connectionSpecs == null) throw new IllegalArgumentException("connectionSpecs == null");
+ this.connectionSpecs = Util.immutableList(connectionSpecs);
+
+ if (proxySelector == null) throw new IllegalArgumentException("proxySelector == null");
+ this.proxySelector = proxySelector;
+
+ this.proxy = proxy;
this.sslSocketFactory = sslSocketFactory;
this.hostnameVerifier = hostnameVerifier;
this.certificatePinner = certificatePinner;
- this.authenticator = authenticator;
- this.protocols = Util.immutableList(protocols);
- this.connectionSpecs = Util.immutableList(connectionSpecs);
- this.proxySelector = proxySelector;
}
- /** Returns the hostname of the origin server. */
- public String getRfc2732Host() {
- return uriHost;
+ /**
+ * Returns a URL with the hostname and port of the origin server. The path, query, and fragment of
+ * this URL are always empty, since they are not significant for planning a route.
+ */
+ public HttpUrl url() {
+ return url;
+ }
+
+ /**
+ * Returns the hostname of the origin server.
+ *
+ * @deprecated prefer {@code address.url().host()}.
+ */
+ @Deprecated
+ public String getUriHost() {
+ return url.host();
}
/**
* Returns the port of the origin server; typically 80 or 443. Unlike
* may {@code getPort()} accessors, this method never returns -1.
+ *
+ * @deprecated prefer {@code address.url().port()}.
*/
+ @Deprecated
public int getUriPort() {
- return uriPort;
+ return url.port();
+ }
+
+ /** Returns the service that will be used to resolve IP addresses for hostnames. */
+ public Dns getDns() {
+ return dns;
}
/** Returns the socket factory for new connections. */
@@ -88,25 +121,7 @@
return socketFactory;
}
- /**
- * 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 client's authenticator. This method never returns null.
- */
+ /** Returns the client's authenticator. */
public Authenticator getAuthenticator() {
return authenticator;
}
@@ -124,14 +139,6 @@
}
/**
- * Returns this address's explicitly-specified HTTP proxy, or null to
- * delegate to the {@linkplain #getProxySelector proxy selector}.
- */
- public Proxy getProxy() {
- return proxy;
- }
-
- /**
* Returns this address's proxy selector. Only used if the proxy is null. If none of this
* selector's proxies are reachable, a direct connection will be attempted.
*/
@@ -140,8 +147,24 @@
}
/**
- * Returns this address's certificate pinner. Only used for secure connections.
+ * Returns this address's explicitly-specified HTTP proxy, or null to
+ * delegate to the {@linkplain #getProxySelector proxy selector}.
*/
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /** 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 this address's certificate pinner, or null if this is not an HTTPS address. */
public CertificatePinner getCertificatePinner() {
return certificatePinner;
}
@@ -149,32 +172,32 @@
@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
+ return this.url.equals(that.url)
+ && this.dns.equals(that.dns)
+ && this.authenticator.equals(that.authenticator)
+ && this.protocols.equals(that.protocols)
+ && this.connectionSpecs.equals(that.connectionSpecs)
+ && this.proxySelector.equals(that.proxySelector)
+ && equal(this.proxy, that.proxy)
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier)
- && equal(this.certificatePinner, that.certificatePinner)
- && equal(this.authenticator, that.authenticator)
- && equal(this.protocols, that.protocols)
- && equal(this.connectionSpecs, that.connectionSpecs)
- && equal(this.proxySelector, that.proxySelector);
+ && equal(this.certificatePinner, that.certificatePinner);
}
return false;
}
@Override public int hashCode() {
int result = 17;
- result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
- 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 + (certificatePinner != null ? certificatePinner.hashCode() : 0);
+ result = 31 * result + url.hashCode();
+ result = 31 * result + dns.hashCode();
result = 31 * result + authenticator.hashCode();
result = 31 * result + protocols.hashCode();
result = 31 * result + connectionSpecs.hashCode();
result = 31 * result + proxySelector.hashCode();
+ result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+ result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
+ result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+ result = 31 * result + (certificatePinner != null ? certificatePinner.hashCode() : 0);
return result;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
index 33561ba..651bd0d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Call.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -19,6 +19,7 @@
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.RequestException;
import com.squareup.okhttp.internal.http.RouteException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.logging.Level;
@@ -119,7 +120,15 @@
*/
public void cancel() {
canceled = true;
- if (engine != null) engine.disconnect();
+ if (engine != null) engine.cancel();
+ }
+
+ /**
+ * Returns true if this call has been either {@linkplain #execute() executed} or {@linkplain
+ * #enqueue(Callback) enqueued}. It is an error to execute a call more than once.
+ */
+ public synchronized boolean isExecuted() {
+ return executed;
}
public boolean isCanceled() {
@@ -172,7 +181,8 @@
// Do not signal the callback twice!
logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
} else {
- responseCallback.onFailure(engine.getRequest(), e);
+ Request request = engine == null ? originalRequest : engine.getRequest();
+ responseCallback.onFailure(request, e);
}
} finally {
client.getDispatcher().finished(this);
@@ -215,14 +225,22 @@
}
@Override public Response proceed(Request request) throws IOException {
+ // If there's another interceptor in the chain, call that.
if (index < client.interceptors().size()) {
- // There's another interceptor in the chain. Call that.
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
- return client.interceptors().get(index).intercept(chain);
- } else {
- // No more interceptors. Do HTTP.
- return getResponse(request, forWebSocket);
+ Interceptor interceptor = client.interceptors().get(index);
+ Response interceptedResponse = interceptor.intercept(chain);
+
+ if (interceptedResponse == null) {
+ throw new NullPointerException("application interceptor " + interceptor
+ + " returned null");
+ }
+
+ return interceptedResponse;
}
+
+ // No more interceptors. Do HTTP.
+ return getResponse(request, forWebSocket);
}
}
@@ -254,18 +272,20 @@
}
// Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
- engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null, null);
+ engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);
int followUpCount = 0;
while (true) {
if (canceled) {
- engine.releaseConnection();
+ engine.releaseStreamAllocation();
throw new IOException("Canceled");
}
+ boolean releaseConnection = true;
try {
engine.sendRequest();
engine.readResponse();
+ releaseConnection = false;
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
@@ -273,6 +293,7 @@
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = engine.recover(e);
if (retryEngine != null) {
+ releaseConnection = false;
engine = retryEngine;
continue;
}
@@ -282,12 +303,19 @@
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = engine.recover(e, null);
if (retryEngine != null) {
+ releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e;
+ } finally {
+ // We're throwing an unchecked exception. Release any resources.
+ if (releaseConnection) {
+ StreamAllocation streamAllocation = engine.close();
+ streamAllocation.release();
+ }
}
Response response = engine.getResponse();
@@ -295,22 +323,25 @@
if (followUp == null) {
if (!forWebSocket) {
- engine.releaseConnection();
+ engine.releaseStreamAllocation();
}
return response;
}
+ StreamAllocation streamAllocation = engine.close();
+
if (++followUpCount > MAX_FOLLOW_UPS) {
+ streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (!engine.sameConnection(followUp.httpUrl())) {
- engine.releaseConnection();
+ streamAllocation.release();
+ streamAllocation = null;
}
- Connection connection = engine.close();
request = followUp;
- engine = new HttpEngine(client, request, false, false, forWebSocket, connection, null, null,
+ engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
response);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
index 15a2952..bd3df19 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
@@ -54,7 +54,7 @@
*
* String hostname = "publicobject.com";
* CertificatePinner certificatePinner = new CertificatePinner.Builder()
- * .add(hostname, "sha1/BOGUSPIN")
+ * .add(hostname, "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
* .build();
* OkHttpClient client = new OkHttpClient();
* client.setCertificatePinner(certificatePinner);
@@ -74,7 +74,7 @@
* sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
* sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
* Pinned certificates for publicobject.com:
- * sha1/BOGUSPIN
+ * sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=
* at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
* at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
* at com.squareup.okhttp.Connection.connect(Connection.java)
@@ -135,7 +135,7 @@
private final Map<String, Set<ByteString>> hostnameToPins;
private CertificatePinner(Builder builder) {
- hostnameToPins = Util.immutableMap(builder.hostnameToPins);
+ this.hostnameToPins = Util.immutableMap(builder.hostnameToPins);
}
/**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index a778747..203b510 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -16,500 +16,69 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.ConnectionSpecSelector;
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.framed.FramedConnection;
-import com.squareup.okhttp.internal.http.FramedTransport;
-import com.squareup.okhttp.internal.http.HttpConnection;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.HttpTransport;
-import com.squareup.okhttp.internal.http.OkHeaders;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
-import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-import java.io.IOException;
-import java.net.Proxy;
import java.net.Socket;
-import java.net.UnknownServiceException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import okio.BufferedSink;
-import okio.BufferedSource;
-import okio.Source;
-
-import static com.squareup.okhttp.internal.Util.closeQuietly;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
/**
- * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be
- * used for multiple HTTP request/response exchanges. Connections may be direct
- * to the origin server or via a proxy.
+ * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be used for multiple
+ * HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.
*
- * <p>Typically instances of this class are created, connected and exercised
- * automatically by the HTTP client. Applications may use this class to monitor
- * HTTP connections as members of a {@linkplain ConnectionPool connection pool}.
+ * <p>Typically instances of this class are created, connected and exercised automatically by the
+ * HTTP client. Applications may use this class to monitor HTTP connections as members of a
+ * {@linkplain ConnectionPool connection pool}.
*
- * <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
- * which isn't so much a connection as a single request/response exchange.
+ * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, which isn't so much a
+ * connection as a single request/response exchange.
*
* <h3>Modern TLS</h3>
- * There are tradeoffs when selecting which options to include when negotiating
- * a secure connection to a remote host. Newer TLS options are quite useful:
+ * 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>Application Layer Protocol Negotiation (ALPN) enables the HTTPS port
- * (443) to be used for different HTTP and SPDY protocols.
+ * <li>Server Name Indication (SNI) enables one IP address to negotiate secure connections for
+ * multiple domain names.
+ * <li>Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used for
+ * different HTTP and SPDY protocols.
* </ul>
- * Unfortunately, older HTTPS servers refuse to connect when such options are
- * presented. Rather than avoiding these options entirely, this class allows a
- * connection to be attempted with modern options and then retried without them
- * should the attempt fail.
+ * Unfortunately, older HTTPS servers refuse to connect when such options are presented. Rather than
+ * avoiding these options entirely, this class allows a connection to be attempted with modern
+ * options and then retried without them should the attempt fail.
+ *
+ * <h3>Connection Reuse</h3>
+ * <p>Each connection can carry a varying number streams, depending on the underlying protocol being
+ * used. HTTP/1.x connections can carry either zero or one streams. HTTP/2 connections can carry any
+ * number of streams, dynamically configured with {@code SETTINGS_MAX_CONCURRENT_STREAMS}. A
+ * connection currently carrying zero streams is an idle stream. We keep it alive because reusing an
+ * existing connection is typically faster than establishing a new one.
+ *
+ * <p>When a single logical call requires multiple streams due to redirects or authorization
+ * challenges, we prefer to use the same physical connection for all streams in the sequence. There
+ * are potential performance and behavior consequences to this preference. To support this feature,
+ * this class separates <i>allocations</i> from <i>streams</i>. An allocation is created by a call,
+ * used for one or more streams, and then released. An allocated connection won't be stolen by
+ * other calls while a redirect or authorization challenge is being handled.
+ *
+ * <p>When the maximum concurrent streams limit is reduced, some allocations will be rescinded.
+ * Attempting to create new streams on these allocations will fail.
+ *
+ * <p>Note that an allocation may be released before its stream is completed. This is intended to
+ * make bookkeeping easier for the caller: releasing the allocation as soon as the terminal stream
+ * has been found. But only complete the stream once its data stream has been exhausted.
*/
-public final class Connection {
- private final ConnectionPool pool;
- private final Route route;
-
- private Socket socket;
- private boolean connected = false;
- private HttpConnection httpConnection;
- private FramedConnection framedConnection;
- private Protocol protocol = Protocol.HTTP_1_1;
- private long idleStartTimeNs;
- private Handshake handshake;
- private int recycleCount;
-
- /**
- * The object that owns this connection. Null if it is shared (for SPDY),
- * belongs to a pool, or has been discarded. Guarded by {@code pool}, which
- * clears the owner when an incoming connection is recycled.
- */
- private Object owner;
-
- public Connection(ConnectionPool pool, Route route) {
- this.pool = pool;
- this.route = route;
- }
-
- Object getOwner() {
- synchronized (pool) {
- return owner;
- }
- }
-
- void setOwner(Object owner) {
- if (isFramed()) return; // Framed connections are shared.
- synchronized (pool) {
- if (this.owner != null) throw new IllegalStateException("Connection already has an owner!");
- this.owner = owner;
- }
- }
-
- /**
- * Attempts to clears the owner of this connection. Returns true if the owner
- * was cleared and the connection can be pooled or reused. This will return
- * false if the connection cannot be pooled or reused, such as if it was
- * closed with {@link #closeIfOwnedBy}.
- */
- boolean clearOwner() {
- synchronized (pool) {
- if (owner == null) {
- // No owner? Don't reuse this connection.
- return false;
- }
-
- owner = null;
- return true;
- }
- }
-
- /**
- * Closes this connection if it is currently owned by {@code owner}. This also
- * strips the ownership of the connection so it cannot be pooled or reused.
- */
- void closeIfOwnedBy(Object owner) throws IOException {
- if (isFramed()) throw new IllegalStateException();
- synchronized (pool) {
- if (this.owner != owner) {
- return; // Wrong owner. Perhaps a late disconnect?
- }
-
- this.owner = null; // Drop the owner so the connection won't be reused.
- }
-
- // Don't close() inside the synchronized block.
- if (socket != null) {
- socket.close();
- }
- }
-
- void connect(int connectTimeout, int readTimeout, int writeTimeout, Request request,
- List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
- if (connected) throw new IllegalStateException("already connected");
-
- RouteException routeException = null;
- ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
- Proxy proxy = route.getProxy();
- Address address = route.getAddress();
-
- if (route.address.getSslSocketFactory() == null
- && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
- throw new RouteException(new UnknownServiceException(
- "CLEARTEXT communication not supported: " + connectionSpecs));
- }
-
- while (!connected) {
- try {
- socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
- ? address.getSocketFactory().createSocket()
- : new Socket(proxy);
- connectSocket(connectTimeout, readTimeout, writeTimeout, request,
- connectionSpecSelector);
- connected = true; // Success!
- } catch (IOException e) {
- Util.closeQuietly(socket);
- socket = null;
-
- if (routeException == null) {
- routeException = new RouteException(e);
- } else {
- routeException.addConnectException(e);
- }
-
- if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
- throw routeException;
- }
- }
- }
- }
-
- /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
- private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
- Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException {
- socket.setSoTimeout(readTimeout);
- Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout);
-
- if (route.address.getSslSocketFactory() != null) {
- connectTls(readTimeout, writeTimeout, request, connectionSpecSelector);
- }
-
- if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
- socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
- framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket)
- .protocol(protocol).build();
- framedConnection.sendConnectionPreface();
- } else {
- httpConnection = new HttpConnection(pool, this, socket);
- }
- }
-
- private void connectTls(int readTimeout, int writeTimeout, Request request,
- ConnectionSpecSelector connectionSpecSelector) throws IOException {
- if (route.requiresTunnel()) {
- createTunnel(readTimeout, writeTimeout, request);
- }
-
- Address address = route.getAddress();
- SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
- boolean success = false;
- SSLSocket sslSocket = null;
- try {
- // Create the wrapper over the connected socket.
- sslSocket = (SSLSocket) sslSocketFactory.createSocket(
- socket, address.getRfc2732Host(), address.getUriPort(), true /* autoClose */);
-
- // Configure the socket's ciphers, TLS versions, and extensions.
- ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
- if (connectionSpec.supportsTlsExtensions()) {
- Platform.get().configureTlsExtensions(
- sslSocket, address.getRfc2732Host(), address.getProtocols());
- }
-
- // Force handshake. This can throw!
- sslSocket.startHandshake();
- Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
-
- // Verify that the socket's certificates are acceptable for the target host.
- if (!address.getHostnameVerifier().verify(address.getRfc2732Host(), sslSocket.getSession())) {
- X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
- throw new SSLPeerUnverifiedException("Hostname " + address.getRfc2732Host() + " not verified:"
- + "\n certificate: " + CertificatePinner.pin(cert)
- + "\n DN: " + cert.getSubjectDN().getName()
- + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
- }
-
- // Check that the certificate pinner is satisfied by the certificates presented.
- address.getCertificatePinner().check(address.getRfc2732Host(),
- unverifiedHandshake.peerCertificates());
-
- // Success! Save the handshake and the ALPN protocol.
- String maybeProtocol = connectionSpec.supportsTlsExtensions()
- ? Platform.get().getSelectedProtocol(sslSocket)
- : null;
- protocol = maybeProtocol != null
- ? Protocol.get(maybeProtocol)
- : Protocol.HTTP_1_1;
- handshake = unverifiedHandshake;
- socket = sslSocket;
- success = true;
- } catch (AssertionError e) {
- if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
- throw e;
- } finally {
- if (sslSocket != null) {
- Platform.get().afterHandshake(sslSocket);
- }
- if (!success) {
- closeQuietly(sslSocket);
- }
- }
- }
-
- /**
- * 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 createTunnel(int readTimeout, int writeTimeout, Request request) throws IOException {
- // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
- Request tunnelRequest = createTunnelRequest(request);
- HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
- tunnelConnection.setTimeouts(readTimeout, writeTimeout);
- HttpUrl url = tunnelRequest.httpUrl();
- String requestLine = "CONNECT " + url.rfc2732host() + ":" + url.port() + " HTTP/1.1";
- while (true) {
- tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
- tunnelConnection.flush();
- Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
- // The response body from a CONNECT should be empty, but if it is not then we should consume
- // it before proceeding.
- long contentLength = OkHeaders.contentLength(response);
- if (contentLength == -1L) {
- contentLength = 0L;
- }
- Source body = tunnelConnection.newFixedLengthSource(contentLength);
- Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
- body.close();
-
- switch (response.code()) {
- case HTTP_OK:
- // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
- // that happens, then we will have buffered bytes that are needed by the SSLSocket!
- // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
- // that it will almost certainly fail because the proxy has sent unexpected data.
- if (tunnelConnection.bufferSize() > 0) {
- throw new IOException("TLS tunnel buffered too many bytes!");
- }
- return;
-
- case HTTP_PROXY_AUTH:
- tunnelRequest = OkHeaders.processAuthHeader(
- route.getAddress().getAuthenticator(), response, route.getProxy());
- if (tunnelRequest != null) continue;
- throw new IOException("Failed to authenticate with proxy");
-
- default:
- throw new IOException(
- "Unexpected response code for CONNECT: " + response.code());
- }
- }
- }
-
- /**
- * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
- * no tunnel is necessary. Everything in the tunnel request is sent
- * unencrypted to the proxy server, so tunnels include only the minimum set of
- * headers. This avoids sending potentially sensitive data like HTTP cookies
- * to the proxy unencrypted.
- */
- private Request createTunnelRequest(Request request) throws IOException {
- HttpUrl tunnelUrl = new HttpUrl.Builder()
- .scheme("https")
- .host(request.httpUrl().host())
- .port(request.httpUrl().port())
- .build();
- Request.Builder result = new Request.Builder()
- .url(tunnelUrl)
- .header("Host", Util.hostHeader(tunnelUrl))
- .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid.
-
- // Copy over the User-Agent header if it exists.
- String userAgent = request.header("User-Agent");
- if (userAgent != null) {
- result.header("User-Agent", userAgent);
- }
-
- // Copy over the Proxy-Authorization header if it exists.
- String proxyAuthorization = request.header("Proxy-Authorization");
- if (proxyAuthorization != null) {
- result.header("Proxy-Authorization", proxyAuthorization);
- }
-
- return result.build();
- }
-
- /**
- * Connects this connection if it isn't already. This creates tunnels, shares
- * the connection with the connection pool, and configures timeouts.
- */
- void connectAndSetOwner(OkHttpClient client, Object owner, Request request)
- throws RouteException {
- setOwner(owner);
-
- if (!isConnected()) {
- List<ConnectionSpec> connectionSpecs = route.address.getConnectionSpecs();
- connect(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(),
- request, connectionSpecs, client.getRetryOnConnectionFailure());
- if (isFramed()) {
- client.getConnectionPool().share(this);
- }
- client.routeDatabase().connected(getRoute());
- }
-
- setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
- }
-
- /** Returns true if {@link #connect} has been attempted on this connection. */
- boolean isConnected() {
- return connected;
- }
-
+public interface Connection {
/** Returns the route used by this connection. */
- public Route getRoute() {
- return route;
- }
+ Route getRoute();
/**
* Returns the socket that this connection uses, or null if the connection
* is not currently connected.
*/
- public Socket getSocket() {
- return socket;
- }
+ Socket getSocket();
- BufferedSource rawSource() {
- if (httpConnection == null) throw new UnsupportedOperationException();
- return httpConnection.rawSource();
- }
-
- BufferedSink rawSink() {
- if (httpConnection == null) throw new UnsupportedOperationException();
- return httpConnection.rawSink();
- }
-
- /** Returns true if this connection is alive. */
- boolean isAlive() {
- return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
- }
+ Handshake getHandshake();
/**
- * Returns true if we are confident that we can read data from this
- * connection. This is more expensive and more accurate than {@link
- * #isAlive()}; callers should check {@link #isAlive()} first.
+ * Returns the protocol negotiated by this connection, or {@link Protocol#HTTP_1_1} if no protocol
+ * has been negotiated. This method returns {@link Protocol#HTTP_1_1} even if the remote peer is
+ * using {@link Protocol#HTTP_1_0}.
*/
- boolean isReadable() {
- if (httpConnection != null) return httpConnection.isReadable();
- return true; // Framed connections, and connections before connect() are both optimistic.
- }
-
- void resetIdleStartTime() {
- if (framedConnection != null) throw new IllegalStateException("framedConnection != null");
- this.idleStartTimeNs = System.nanoTime();
- }
-
- /** Returns true if this connection is idle. */
- boolean isIdle() {
- return framedConnection == null || framedConnection.isIdle();
- }
-
- /**
- * Returns the time in ns when this connection became idle. Undefined if
- * this connection is not idle.
- */
- long getIdleStartTimeNs() {
- return framedConnection == null ? idleStartTimeNs : framedConnection.getIdleStartTimeNs();
- }
-
- public Handshake getHandshake() {
- return handshake;
- }
-
- /** Returns the transport appropriate for this connection. */
- Transport newTransport(HttpEngine httpEngine) throws IOException {
- return (framedConnection != null)
- ? new FramedTransport(httpEngine, framedConnection)
- : new HttpTransport(httpEngine, httpConnection);
- }
-
- /**
- * Returns true if this is a SPDY connection. Such connections can be used
- * in multiple HTTP requests simultaneously.
- */
- boolean isFramed() {
- return framedConnection != null;
- }
-
- /**
- * Returns the protocol negotiated by this connection, or {@link
- * Protocol#HTTP_1_1} if no protocol has been negotiated.
- */
- public Protocol getProtocol() {
- return protocol;
- }
-
- /**
- * Sets the protocol negotiated by this connection. Typically this is used
- * when an HTTP/1.1 request is sent and an HTTP/1.0 response is received.
- */
- void setProtocol(Protocol protocol) {
- if (protocol == null) throw new IllegalArgumentException("protocol == null");
- this.protocol = protocol;
- }
-
- void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis)
- throws RouteException {
- if (!connected) throw new IllegalStateException("setTimeouts - not connected");
-
- // Don't set timeouts on shared SPDY connections.
- if (httpConnection != null) {
- try {
- socket.setSoTimeout(readTimeoutMillis);
- } catch (IOException e) {
- throw new RouteException(e);
- }
- httpConnection.setTimeouts(readTimeoutMillis, writeTimeoutMillis);
- }
- }
-
- void incrementRecycleCount() {
- recycleCount++;
- }
-
- /**
- * Returns the number of times this connection has been returned to the
- * connection pool.
- */
- int recycleCount() {
- return recycleCount;
- }
-
- @Override public String toString() {
- return "Connection{"
- + route.address.uriHost + ":" + route.address.uriPort
- + ", proxy="
- + route.proxy
- + " hostAddress="
- + route.inetSocketAddress.getAddress().getHostAddress()
- + " cipherSuite="
- + (handshake != null ? handshake.cipherSuite() : "none")
- + " protocol="
- + protocol
- + '}';
- }
+ Protocol getProtocol();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index da3ac73..6f8efc4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -16,13 +16,17 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RouteDatabase;
import com.squareup.okhttp.internal.Util;
-import java.net.SocketException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
+import java.lang.ref.Reference;
+import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.LinkedList;
+import java.util.Deque;
+import java.util.Iterator;
import java.util.List;
-import java.util.ListIterator;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
@@ -31,8 +35,8 @@
/**
* Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
* requests that share the same {@link com.squareup.okhttp.Address} may share a
- * {@link com.squareup.okhttp.Connection}. This class implements the policy of
- * which connections to keep open for future use.
+ * {@link 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:
@@ -60,7 +64,8 @@
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)
+ long keepAliveDurationMs = keepAliveDuration != null
+ ? Long.parseLong(keepAliveDuration)
: DEFAULT_KEEP_ALIVE_DURATION_MS;
if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
systemDefault = new ConnectionPool(0, keepAliveDurationMs);
@@ -71,43 +76,73 @@
}
}
- /** The maximum number of idle connections for each address. */
- private final int maxIdleConnections;
- private final long keepAliveDurationNs;
-
- private final LinkedList<Connection> connections = new LinkedList<>();
-
/**
* A background thread is used to cleanup expired connections. There will be, at most, a single
- * thread running per connection pool.
- *
- * <p>A {@link ThreadPoolExecutor} is used and not a
- * {@link java.util.concurrent.ScheduledThreadPoolExecutor}; ScheduledThreadPoolExecutors do not
- * shrink. This executor shrinks the thread pool after a period of inactivity, and starts threads
- * as needed. Delays are instead handled by the {@link #connectionsCleanupRunnable}. It is
- * important that the {@link #connectionsCleanupRunnable} stops eventually, otherwise it will pin
- * the thread, and thus the connection pool, in memory.
+ * thread running per connection pool. We use a thread pool executor because it can shrink to
+ * zero threads, permitting this pool to be garbage collected.
*/
- private Executor executor = new ThreadPoolExecutor(
+ private final Executor executor = new ThreadPoolExecutor(
0 /* corePoolSize */, 1 /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
- private final Runnable connectionsCleanupRunnable = new Runnable() {
+ /** The maximum number of idle connections for each address. */
+ private final int maxIdleConnections;
+ private final long keepAliveDurationNs;
+ private Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
- runCleanupUntilPoolIsEmpty();
+ while (true) {
+ long waitNanos = cleanup(System.nanoTime());
+ if (waitNanos == -1) return;
+ if (waitNanos > 0) {
+ long waitMillis = waitNanos / 1000000L;
+ waitNanos -= (waitMillis * 1000000L);
+ synchronized (ConnectionPool.this) {
+ try {
+ ConnectionPool.this.wait(waitMillis, (int) waitNanos);
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+ }
}
};
+ private final Deque<RealConnection> connections = new ArrayDeque<>();
+ final RouteDatabase routeDatabase = new RouteDatabase();
+
public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
+ this(maxIdleConnections, keepAliveDurationMs, TimeUnit.MILLISECONDS);
+ }
+
+ public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
- this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
+ this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
+
+ // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
+ if (keepAliveDuration <= 0) {
+ throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
+ }
}
public static ConnectionPool getDefault() {
return systemDefault;
}
- /** Returns total number of connections in the pool. */
+ /** Returns the number of idle connections in the pool. */
+ public synchronized int getIdleConnectionCount() {
+ int total = 0;
+ for (RealConnection connection : connections) {
+ if (connection.allocations.isEmpty()) total++;
+ }
+ return total;
+ }
+
+ /**
+ * Returns total number of connections in the pool. Note that prior to OkHttp 2.7 this included
+ * only idle connections and SPDY connections. In OkHttp 2.7 this includes all connections, both
+ * active and inactive. Use {@link #getIdleConnectionCount()} to count connections not currently
+ * in use.
+ */
public synchronized int getConnectionCount() {
return connections.size();
}
@@ -121,8 +156,8 @@
/** Returns total number of multiplexed connections in the pool. */
public synchronized int getMultiplexedConnectionCount() {
int total = 0;
- for (Connection connection : connections) {
- if (connection.isFramed()) total++;
+ for (RealConnection connection : connections) {
+ if (connection.isMultiplexed()) total++;
}
return total;
}
@@ -133,205 +168,156 @@
}
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
- public synchronized Connection get(Address address) {
- Connection foundConnection = null;
- for (ListIterator<Connection> i = connections.listIterator(connections.size());
- i.hasPrevious(); ) {
- Connection connection = i.previous();
- if (!connection.getRoute().getAddress().equals(address)
- || !connection.isAlive()
- || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
- continue;
+ RealConnection get(Address address, StreamAllocation streamAllocation) {
+ assert (Thread.holdsLock(this));
+ for (RealConnection connection : connections) {
+ // TODO(jwilson): this is awkward. We're already holding a lock on 'this', and
+ // connection.allocationLimit() may also lock the FramedConnection.
+ if (connection.allocations.size() < connection.allocationLimit()
+ && address.equals(connection.getRoute().address)
+ && !connection.noNewStreams) {
+ streamAllocation.acquire(connection);
+ return connection;
}
- i.remove();
- if (!connection.isFramed()) {
- try {
- Platform.get().tagSocket(connection.getSocket());
- } catch (SocketException e) {
- Util.closeQuietly(connection.getSocket());
- // When unable to tag, skip recycling and close
- Platform.get().logW("Unable to tagSocket(): " + e);
+ }
+ return null;
+ }
+
+ void put(RealConnection connection) {
+ assert (Thread.holdsLock(this));
+ if (connections.isEmpty()) {
+ executor.execute(cleanupRunnable);
+ }
+ connections.add(connection);
+ }
+
+ /**
+ * Notify this pool that {@code connection} has become idle. Returns true if the connection
+ * has been removed from the pool and should be closed.
+ */
+ boolean connectionBecameIdle(RealConnection connection) {
+ assert (Thread.holdsLock(this));
+ if (connection.noNewStreams || maxIdleConnections == 0) {
+ connections.remove(connection);
+ return true;
+ } else {
+ notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
+ return false;
+ }
+ }
+
+ /** Close and remove all idle connections in the pool. */
+ public void evictAll() {
+ List<RealConnection> evictedConnections = new ArrayList<>();
+ synchronized (this) {
+ for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
+ RealConnection connection = i.next();
+ if (connection.allocations.isEmpty()) {
+ connection.noNewStreams = true;
+ evictedConnections.add(connection);
+ i.remove();
+ }
+ }
+ }
+
+ for (RealConnection connection : evictedConnections) {
+ Util.closeQuietly(connection.getSocket());
+ }
+ }
+
+ /**
+ * Performs maintenance on this pool, evicting the connection that has been idle the longest if
+ * either it has exceeded the keep alive limit or the idle connections limit.
+ *
+ * <p>Returns the duration in nanos to sleep until the next scheduled call to this method.
+ * Returns -1 if no further cleanups are required.
+ */
+ long cleanup(long now) {
+ int inUseConnectionCount = 0;
+ int idleConnectionCount = 0;
+ RealConnection longestIdleConnection = null;
+ long longestIdleDurationNs = Long.MIN_VALUE;
+
+ // Find either a connection to evict, or the time that the next eviction is due.
+ synchronized (this) {
+ for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
+ RealConnection connection = i.next();
+
+ // If the connection is in use, keep searching.
+ if (pruneAndGetAllocationCount(connection, now) > 0) {
+ inUseConnectionCount++;
continue;
}
- }
- foundConnection = connection;
- break;
- }
- if (foundConnection != null && foundConnection.isFramed()) {
- connections.addFirst(foundConnection); // Add it back after iteration.
- }
+ idleConnectionCount++;
- 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.
- */
- void recycle(Connection connection) {
- if (connection.isFramed()) {
- return;
- }
-
- if (!connection.clearOwner()) {
- return; // This connection isn't eligible for reuse.
- }
-
- if (!connection.isAlive()) {
- Util.closeQuietly(connection.getSocket());
- 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.getSocket());
- return;
- }
-
- synchronized (this) {
- addConnection(connection);
- connection.incrementRecycleCount();
- connection.resetIdleStartTime();
- }
- }
-
- private void addConnection(Connection connection) {
- boolean empty = connections.isEmpty();
- connections.addFirst(connection);
- if (empty) {
- executor.execute(connectionsCleanupRunnable);
- } else {
- notifyAll();
- }
- }
-
- /**
- * Shares the SPDY connection with the pool. Callers to this method may
- * continue to use {@code connection}.
- */
- void share(Connection connection) {
- if (!connection.isFramed()) throw new IllegalArgumentException();
- if (!connection.isAlive()) return;
- synchronized (this) {
- addConnection(connection);
- }
- }
-
- /** Close and remove all connections in the pool. */
- public void evictAll() {
- List<Connection> toEvict;
- synchronized (this) {
- toEvict = new ArrayList<>(connections);
- connections.clear();
- notifyAll();
- }
-
- for (int i = 0, size = toEvict.size(); i < size; i++) {
- Util.closeQuietly(toEvict.get(i).getSocket());
- }
- }
-
- private void runCleanupUntilPoolIsEmpty() {
- while (true) {
- if (!performCleanup()) return; // Halt cleanup.
- }
- }
-
- /**
- * Attempts to make forward progress on connection eviction. There are three possible outcomes:
- *
- * <h3>The pool is empty.</h3>
- * In this case, this method returns false and the eviction job should exit because there are no
- * further cleanup tasks coming. (If additional connections are added to the pool, another cleanup
- * job must be enqueued.)
- *
- * <h3>Connections were evicted.</h3>
- * At least one connections was eligible for immediate eviction and was evicted. The method
- * returns true and cleanup should continue.
- *
- * <h3>We waited to evict.</h3>
- * None of the pooled connections were eligible for immediate eviction. Instead, we waited until
- * either a connection became eligible for eviction, or the connections list changed. In either
- * case, the method returns true and cleanup should continue.
- */
- // VisibleForTesting
- boolean performCleanup() {
- List<Connection> evictableConnections;
-
- synchronized (this) {
- if (connections.isEmpty()) return false; // Halt cleanup.
-
- evictableConnections = new ArrayList<>();
- int idleConnectionCount = 0;
- long now = System.nanoTime();
- long nanosUntilNextEviction = keepAliveDurationNs;
-
- // Collect connections eligible for immediate eviction.
- for (ListIterator<Connection> i = connections.listIterator(connections.size());
- i.hasPrevious(); ) {
- Connection connection = i.previous();
- long nanosUntilEviction = connection.getIdleStartTimeNs() + keepAliveDurationNs - now;
- if (nanosUntilEviction <= 0 || !connection.isAlive()) {
- i.remove();
- evictableConnections.add(connection);
- } else if (connection.isIdle()) {
- idleConnectionCount++;
- nanosUntilNextEviction = Math.min(nanosUntilNextEviction, nanosUntilEviction);
+ // If the connection is ready to be evicted, we're done.
+ long idleDurationNs = now - connection.idleAtNanos;
+ if (idleDurationNs > longestIdleDurationNs) {
+ longestIdleDurationNs = idleDurationNs;
+ longestIdleConnection = connection;
}
}
- // If the pool has too many idle connections, gather more! Oldest to newest.
- for (ListIterator<Connection> i = connections.listIterator(connections.size());
- i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
- Connection connection = i.previous();
- if (connection.isIdle()) {
- evictableConnections.add(connection);
- i.remove();
- --idleConnectionCount;
- }
- }
+ if (longestIdleDurationNs >= this.keepAliveDurationNs
+ || idleConnectionCount > this.maxIdleConnections) {
+ // We've found a connection to evict. Remove it from the list, then close it below (outside
+ // of the synchronized block).
+ connections.remove(longestIdleConnection);
- // If there's nothing to evict, wait. (This will be interrupted if connections are added.)
- if (evictableConnections.isEmpty()) {
- try {
- long millisUntilNextEviction = nanosUntilNextEviction / (1000 * 1000);
- long remainderNanos = nanosUntilNextEviction - millisUntilNextEviction * (1000 * 1000);
- this.wait(millisUntilNextEviction, (int) remainderNanos);
- return true; // Cleanup continues.
- } catch (InterruptedException ignored) {
- }
+ } else if (idleConnectionCount > 0) {
+ // A connection will be ready to evict soon.
+ return keepAliveDurationNs - longestIdleDurationNs;
+
+ } else if (inUseConnectionCount > 0) {
+ // All connections are in use. It'll be at least the keep alive duration 'til we run again.
+ return keepAliveDurationNs;
+
+ } else {
+ // No connections, idle or in use.
+ return -1;
}
}
- // Actually do the eviction. Note that we avoid synchronized() when closing sockets.
- for (int i = 0, size = evictableConnections.size(); i < size; i++) {
- Connection expiredConnection = evictableConnections.get(i);
- Util.closeQuietly(expiredConnection.getSocket());
+ Util.closeQuietly(longestIdleConnection.getSocket());
+
+ // Cleanup again immediately.
+ return 0;
+ }
+
+ /**
+ * Prunes any leaked allocations and then returns the number of remaining live allocations on
+ * {@code connection}. Allocations are leaked if the connection is tracking them but the
+ * application code has abandoned them. Leak detection is imprecise and relies on garbage
+ * collection.
+ */
+ private int pruneAndGetAllocationCount(RealConnection connection, long now) {
+ List<Reference<StreamAllocation>> references = connection.allocations;
+ for (int i = 0; i < references.size(); ) {
+ Reference<StreamAllocation> reference = references.get(i);
+
+ if (reference.get() != null) {
+ i++;
+ continue;
+ }
+
+ // We've discovered a leaked allocation. This is an application bug.
+ Internal.logger.warning("A connection to " + connection.getRoute().getAddress().url()
+ + " was leaked. Did you forget to close a response body?");
+ references.remove(i);
+ connection.noNewStreams = true;
+
+ // If this was the last allocation, the connection is eligible for immediate eviction.
+ if (references.isEmpty()) {
+ connection.idleAtNanos = now - keepAliveDurationNs;
+ return 0;
+ }
}
- return true; // Cleanup continues.
+ return references.size();
}
- /**
- * Replace the default {@link Executor} with a different one. Only use in tests.
- */
- // VisibleForTesting
- void replaceCleanupExecutorForTests(Executor cleanupExecutor) {
- this.executor = cleanupExecutor;
- }
-
- /**
- * Returns a snapshot of the connections in this pool, ordered from newest to
- * oldest. Only use in tests.
- */
- // VisibleForTesting
- synchronized List<Connection> getConnections() {
- return new ArrayList<>(connections);
+ void setCleanupRunnableForTest(Runnable cleanupRunnable) {
+ this.cleanupRunnable = cleanupRunnable;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
index 5e0f7d8..af63afd 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
@@ -20,14 +20,24 @@
import java.util.List;
import javax.net.ssl.SSLSocket;
+import static com.squareup.okhttp.internal.Util.concat;
+import static com.squareup.okhttp.internal.Util.contains;
+
/**
* Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
* https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
* connection.
+ *
+ * <p>The TLS versions configured in a connection spec are only be used if they are also enabled in
+ * the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used
+ * even if it is present on the connection spec. The same policy also applies to cipher suites.
+ *
+ * <p>Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to
+ * defer all feature selection to the underlying SSL socket.
*/
public final class ConnectionSpec {
- // This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5.
+ // This is a subset of the cipher suites supported in Chrome 46, current as of 2015-11-05.
// All of these suites are available on Android 5.0; earlier releases support a subset of
// these suites. https://github.com/square/okhttp/issues/330
private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
@@ -43,7 +53,6 @@
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
- CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
@@ -67,19 +76,11 @@
/** Unencrypted, unauthenticated connections for {@code http:} URLs. */
public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
- final boolean tls;
-
- /**
- * Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use
- * default set".
- */
+ private final boolean tls;
+ private final boolean supportsTlsExtensions;
private final String[] cipherSuites;
-
- /** Used if tls == true. The TLS protocol versions to use. */
private final String[] tlsVersions;
- final boolean supportsTlsExtensions;
-
private ConnectionSpec(Builder builder) {
this.tls = builder.tls;
this.cipherSuites = builder.cipherSuites;
@@ -92,13 +93,12 @@
}
/**
- * Returns the cipher suites to use for a connection. This method can return {@code null} if the
- * cipher suites enabled by default should be used.
+ * Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL
+ * socket's enabled cipher suites should be used.
*/
public List<CipherSuite> cipherSuites() {
- if (cipherSuites == null) {
- return null;
- }
+ if (cipherSuites == null) return null;
+
CipherSuite[] result = new CipherSuite[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
result[i] = CipherSuite.forJavaName(cipherSuites[i]);
@@ -106,7 +106,13 @@
return Util.immutableList(result);
}
+ /**
+ * Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of
+ * the SSL socket's enabled TLS versions should be used.
+ */
public List<TlsVersion> tlsVersions() {
+ if (tlsVersions == null) return null;
+
TlsVersion[] result = new TlsVersion[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
result[i] = TlsVersion.forJavaName(tlsVersions[i]);
@@ -122,57 +128,40 @@
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
- sslSocket.setEnabledProtocols(specToApply.tlsVersions);
-
- String[] cipherSuitesToEnable = specToApply.cipherSuites;
- // null means "use default set".
- if (cipherSuitesToEnable != null) {
- sslSocket.setEnabledCipherSuites(cipherSuitesToEnable);
+ if (specToApply.tlsVersions != null) {
+ sslSocket.setEnabledProtocols(specToApply.tlsVersions);
+ }
+ if (specToApply.cipherSuites != null) {
+ sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
/**
- * Returns a copy of this that omits cipher suites and TLS versions not enabled by
- * {@code sslSocket}.
+ * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
+ * sslSocket}.
*/
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
- String[] cipherSuitesToEnable = null;
- if (cipherSuites != null) {
- String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites();
- cipherSuitesToEnable =
- Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom);
+ String[] cipherSuitesIntersection = cipherSuites != null
+ ? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
+ : sslSocket.getEnabledCipherSuites();
+ String[] tlsVersionsIntersection = tlsVersions != null
+ ? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
+ : sslSocket.getEnabledProtocols();
+
+ // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
+ // the SCSV cipher is added to signal that a protocol fallback has taken place.
+ if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) {
+ cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
}
- if (isFallback) {
- // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
- // the SCSV cipher is added to signal that a protocol fallback has taken place.
- final String fallbackScsv = "TLS_FALLBACK_SCSV";
- boolean socketSupportsFallbackScsv =
- Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv);
-
- if (socketSupportsFallbackScsv) {
- // Add the SCSV cipher to the set of enabled cipher suites iff it is supported.
- String[] oldEnabledCipherSuites = cipherSuitesToEnable != null
- ? cipherSuitesToEnable
- : sslSocket.getEnabledCipherSuites();
- String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1];
- System.arraycopy(oldEnabledCipherSuites, 0,
- newEnabledCipherSuites, 0, oldEnabledCipherSuites.length);
- newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
- cipherSuitesToEnable = newEnabledCipherSuites;
- }
- }
-
- String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols();
- String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom);
return new Builder(this)
- .cipherSuites(cipherSuitesToEnable)
- .tlsVersions(protocolsToEnable)
+ .cipherSuites(cipherSuitesIntersection)
+ .tlsVersions(tlsVersionsIntersection)
.build();
}
/**
- * Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec.
+ * Returns {@code true} if the socket, as currently configured, supports this connection spec.
* In order for a socket to be compatible the enabled cipher suites and protocols must intersect.
*
* <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
@@ -187,20 +176,17 @@
return false;
}
- String[] enabledProtocols = socket.getEnabledProtocols();
- boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols);
- if (!requiredProtocolsEnabled) {
+ if (tlsVersions != null
+ && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
return false;
}
- boolean requiredCiphersEnabled;
- if (cipherSuites == null) {
- requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0;
- } else {
- String[] enabledCipherSuites = socket.getEnabledCipherSuites();
- requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites);
+ if (cipherSuites != null
+ && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
+ return false;
}
- return requiredCiphersEnabled;
+
+ return true;
}
/**
@@ -220,15 +206,6 @@
return false;
}
- private static <T> boolean contains(T[] array, T value) {
- for (T arrayValue : array) {
- if (Util.equal(value, arrayValue)) {
- return true;
- }
- }
- return false;
- }
-
@Override public boolean equals(Object other) {
if (!(other instanceof ConnectionSpec)) return false;
if (other == this) return true;
@@ -256,16 +233,17 @@
}
@Override public String toString() {
- if (tls) {
- List<CipherSuite> cipherSuites = cipherSuites();
- String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString();
- return "ConnectionSpec(cipherSuites=" + cipherSuitesString
- + ", tlsVersions=" + tlsVersions()
- + ", supportsTlsExtensions=" + supportsTlsExtensions
- + ")";
- } else {
+ if (!tls) {
return "ConnectionSpec()";
}
+
+ String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]";
+ String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]";
+ return "ConnectionSpec("
+ + "cipherSuites=" + cipherSuitesString
+ + ", tlsVersions=" + tlsVersionsString
+ + ", supportsTlsExtensions=" + supportsTlsExtensions
+ + ")";
}
public static final class Builder {
@@ -285,56 +263,58 @@
this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
}
+ public Builder allEnabledCipherSuites() {
+ if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
+ this.cipherSuites = null;
+ return this;
+ }
+
public Builder cipherSuites(CipherSuite... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
- // Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
strings[i] = cipherSuites[i].javaName;
}
- this.cipherSuites = strings;
- return this;
+ return cipherSuites(strings);
}
public Builder cipherSuites(String... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
- if (cipherSuites == null) {
- this.cipherSuites = null;
- } else {
- // This makes a defensive copy!
- this.cipherSuites = cipherSuites.clone();
+ if (cipherSuites.length == 0) {
+ throw new IllegalArgumentException("At least one cipher suite is required");
}
+ this.cipherSuites = cipherSuites.clone(); // Defensive copy.
+ return this;
+ }
+
+ public Builder allEnabledTlsVersions() {
+ if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
+ this.tlsVersions = null;
return this;
}
public Builder tlsVersions(TlsVersion... tlsVersions) {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
- if (tlsVersions.length == 0) {
- throw new IllegalArgumentException("At least one TlsVersion is required");
- }
- // Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
strings[i] = tlsVersions[i].javaName;
}
- this.tlsVersions = strings;
- return this;
+
+ return tlsVersions(strings);
}
public Builder tlsVersions(String... tlsVersions) {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
- if (tlsVersions == null) {
- this.tlsVersions = null;
- } else {
- // This makes a defensive copy!
- this.tlsVersions = tlsVersions.clone();
+ if (tlsVersions.length == 0) {
+ throw new IllegalArgumentException("At least one TLS version is required");
}
+ this.tlsVersions = tlsVersions.clone(); // Defensive copy.
return this;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
index a934670..a669b94 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
@@ -125,7 +125,7 @@
if (Util.equal(tag, call.tag())) {
call.get().canceled = true;
HttpEngine engine = call.get().engine;
- if (engine != null) engine.disconnect();
+ if (engine != null) engine.cancel();
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dns.java b/okhttp/src/main/java/com/squareup/okhttp/Dns.java
new file mode 100644
index 0000000..1ebd392
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dns.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 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 java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A domain name service that resolves IP addresses for host names. Most applications will use the
+ * {@linkplain #SYSTEM system DNS service}, which is the default. Some applications may provide
+ * their own implementation to use a different DNS server, to prefer IPv6 addresses, to prefer IPv4
+ * addresses, or to force a specific known IP address.
+ *
+ * <p>Implementations of this interface must be safe for concurrent use.
+ */
+public interface Dns {
+ /**
+ * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
+ * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
+ */
+ Dns SYSTEM = new Dns() {
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ if (hostname == null) throw new UnknownHostException("hostname == null");
+ return Arrays.asList(InetAddress.getAllByName(hostname));
+ }
+ };
+
+ /**
+ * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp.
+ * If a connection to an address fails, OkHttp will retry the connection with the next address
+ * until either a connection is made, the set of IP addresses is exhausted, or a limit is
+ * exceeded.
+ */
+ List<InetAddress> lookup(String hostname) throws UnknownHostException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
index 8e75e3f..6fbf5b8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
@@ -402,19 +402,6 @@
}
/**
- * Same as {@link #host} except that literal IPv6 addresses are surrounding by square
- * braces. For example, this method will return {@code [::1]} where {@code host} returns
- * {@code ::1}.
- */
- public String rfc2732host() {
- if (host.indexOf(':') == -1) {
- return host;
- }
-
- return "[" + host + "]";
- }
-
- /**
* Returns the explicitly-specified port if one was provided, or the default port for this URL's
* scheme. For example, this returns 8443 for {@code https://square.com:8443/} and 443 for {@code
* https://square.com/}. The result is in {@code [1..65535]}.
@@ -1640,7 +1627,7 @@
*
* @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'.
* @param strict true to encode '%' if it is not the prefix of a valid percent encoding.
- * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded
+ * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded.
* @param asciiOnly true to encode all non-ASCII codepoints.
*/
static String canonicalize(String input, int pos, int limit, String encodeSet,
@@ -1702,9 +1689,9 @@
}
}
- static String canonicalize(String input, String encodeSet, boolean alreadyEncoded, boolean strict,
- boolean plusIsSpace, boolean asciiOnly) {
- return canonicalize(
- input, 0, input.length(), encodeSet, alreadyEncoded, strict, plusIsSpace, asciiOnly);
+ static String canonicalize(String input, String encodeSet, boolean alreadyEncoded,
+ boolean strict, boolean plusIsSpace, boolean asciiOnly) {
+ return canonicalize(input, 0, input.length(),
+ encodeSet, alreadyEncoded, strict, plusIsSpace, asciiOnly);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 4ed8000..aabc2d2 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -17,15 +17,12 @@
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.InternalCache;
-import com.squareup.okhttp.internal.Network;
import com.squareup.okhttp.internal.RouteDatabase;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-import java.io.IOException;
import java.net.CookieHandler;
import java.net.MalformedURLException;
import java.net.Proxy;
@@ -41,8 +38,6 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
-import okio.BufferedSink;
-import okio.BufferedSource;
/**
* Configures and creates HTTP connections. Most applications can use a single
@@ -64,35 +59,6 @@
static {
Internal.instance = new Internal() {
- @Override public Transport newTransport(
- Connection connection, HttpEngine httpEngine) throws IOException {
- return connection.newTransport(httpEngine);
- }
-
- @Override public boolean clearOwner(Connection connection) {
- return connection.clearOwner();
- }
-
- @Override public void closeIfOwnedBy(Connection connection, Object owner) throws IOException {
- connection.closeIfOwnedBy(owner);
- }
-
- @Override public int recycleCount(Connection connection) {
- return connection.recycleCount();
- }
-
- @Override public void setProtocol(Connection connection, Protocol protocol) {
- connection.setProtocol(protocol);
- }
-
- @Override public void setOwner(Connection connection, HttpEngine httpEngine) {
- connection.setOwner(httpEngine);
- }
-
- @Override public boolean isReadable(Connection pooled) {
- return pooled.isReadable();
- }
-
@Override public void addLenient(Headers.Builder builder, String line) {
builder.addLenient(line);
}
@@ -109,25 +75,22 @@
return client.internalCache();
}
- @Override public void recycle(ConnectionPool pool, Connection connection) {
- pool.recycle(connection);
+ @Override public boolean connectionBecameIdle(
+ ConnectionPool pool, RealConnection connection) {
+ return pool.connectionBecameIdle(connection);
}
- @Override public RouteDatabase routeDatabase(OkHttpClient client) {
- return client.routeDatabase();
+ @Override public RealConnection get(
+ ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
+ return pool.get(address, streamAllocation);
}
- @Override public Network network(OkHttpClient client) {
- return client.network;
+ @Override public void put(ConnectionPool pool, RealConnection connection) {
+ pool.put(connection);
}
- @Override public void setNetwork(OkHttpClient client, Network network) {
- client.network = network;
- }
-
- @Override public void connectAndSetOwner(OkHttpClient client, Connection connection,
- HttpEngine owner, Request request) throws RouteException {
- connection.connectAndSetOwner(client, owner, request);
+ @Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
+ return connectionPool.routeDatabase;
}
@Override
@@ -135,24 +98,8 @@
call.enqueue(responseCallback, forWebSocket);
}
- @Override public void callEngineReleaseConnection(Call call) throws IOException {
- call.engine.releaseConnection();
- }
-
- @Override public Connection callEngineGetConnection(Call call) {
- return call.engine.getConnection();
- }
-
- @Override public BufferedSource connectionRawSource(Connection connection) {
- return connection.rawSource();
- }
-
- @Override public BufferedSink connectionRawSink(Connection connection) {
- return connection.rawSink();
- }
-
- @Override public void connectionSetOwner(Connection connection, Object owner) {
- connection.setOwner(owner);
+ @Override public StreamAllocation callEngineGetStreamAllocation(Call call) {
+ return call.engine.streamAllocation;
}
@Override
@@ -190,7 +137,7 @@
private CertificatePinner certificatePinner;
private Authenticator authenticator;
private ConnectionPool connectionPool;
- private Network network;
+ private Dns dns;
private boolean followSslRedirects = true;
private boolean followRedirects = true;
private boolean retryOnConnectionFailure = true;
@@ -221,7 +168,7 @@
this.certificatePinner = okHttpClient.certificatePinner;
this.authenticator = okHttpClient.authenticator;
this.connectionPool = okHttpClient.connectionPool;
- this.network = okHttpClient.network;
+ this.dns = okHttpClient.dns;
this.followSslRedirects = okHttpClient.followSslRedirects;
this.followRedirects = okHttpClient.followRedirects;
this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
@@ -358,6 +305,20 @@
}
/**
+ * Sets the DNS service used to lookup IP addresses for hostnames.
+ *
+ * <p>If unset, the {@link Dns#SYSTEM system-wide default} DNS will be used.
+ */
+ public OkHttpClient setDns(Dns dns) {
+ this.dns = dns;
+ return this;
+ }
+
+ public Dns getDns() {
+ return dns;
+ }
+
+ /**
* Sets the socket factory used to create connections. OkHttp only uses
* the parameterless {@link SocketFactory#createSocket() createSocket()}
* method to create unconnected sockets. Overriding this method,
@@ -647,8 +608,8 @@
if (result.connectionSpecs == null) {
result.connectionSpecs = DEFAULT_CONNECTION_SPECS;
}
- if (result.network == null) {
- result.network = Network.DEFAULT;
+ if (result.dns == null) {
+ result.dns = Dns.SYSTEM;
}
return result;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index 2417c13..e099267 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -189,6 +189,9 @@
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for
* multiply-valued headers like "Cookie".
+ *
+ * <p>Note that for some headers including {@code Content-Length} and {@code Content-Encoding},
+ * OkHttp may replace {@code value} with a header derived from the request body.
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
index bfa95c4..512aa0d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
@@ -30,7 +30,7 @@
final String javaName;
- private TlsVersion(String javaName) {
+ TlsVersion(String javaName) {
this.javaName = javaName;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
index 21bcbf5..03bc1c5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
@@ -15,26 +15,20 @@
*/
package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Address;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Callback;
-import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.ConnectionSpec;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
-import java.io.IOException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.util.logging.Logger;
import javax.net.ssl.SSLSocket;
-import okio.BufferedSink;
-import okio.BufferedSource;
/**
* Escalate internal APIs in {@code com.squareup.okhttp} so they can be used
@@ -51,21 +45,6 @@
public static Internal instance;
- public abstract Transport newTransport(Connection connection, HttpEngine httpEngine)
- throws IOException;
-
- public abstract boolean clearOwner(Connection connection);
-
- public abstract void closeIfOwnedBy(Connection connection, Object owner) throws IOException;
-
- public abstract int recycleCount(Connection connection);
-
- public abstract void setProtocol(Connection connection, Protocol protocol);
-
- public abstract void setOwner(Connection connection, HttpEngine httpEngine);
-
- public abstract boolean isReadable(Connection pooled);
-
public abstract void addLenient(Headers.Builder builder, String line);
public abstract void addLenient(Headers.Builder builder, String name, String value);
@@ -74,16 +53,14 @@
public abstract InternalCache internalCache(OkHttpClient client);
- public abstract void recycle(ConnectionPool pool, Connection connection);
+ public abstract RealConnection get(
+ ConnectionPool pool, Address address, StreamAllocation streamAllocation);
- public abstract RouteDatabase routeDatabase(OkHttpClient client);
+ public abstract void put(ConnectionPool pool, RealConnection connection);
- public abstract Network network(OkHttpClient client);
+ public abstract boolean connectionBecameIdle(ConnectionPool pool, RealConnection connection);
- public abstract void setNetwork(OkHttpClient client, Network network);
-
- public abstract void connectAndSetOwner(OkHttpClient client, Connection connection,
- HttpEngine owner, Request request) throws RouteException;
+ public abstract RouteDatabase routeDatabase(ConnectionPool connectionPool);
public abstract void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket,
boolean isFallback);
@@ -93,9 +70,5 @@
// TODO delete the following when web sockets move into the main package.
public abstract void callEnqueue(Call call, Callback responseCallback, boolean forWebSocket);
- public abstract void callEngineReleaseConnection(Call call) throws IOException;
- public abstract Connection callEngineGetConnection(Call call);
- public abstract BufferedSource connectionRawSource(Connection connection);
- public abstract BufferedSink connectionRawSink(Connection connection);
- public abstract void connectionSetOwner(Connection connection, Object owner);
+ public abstract StreamAllocation callEngineGetStreamAllocation(Call call);
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
deleted file mode 100644
index a007065..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2012 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;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * Services specific to the host device's network interface. Prefer this over {@link
- * InetAddress#getAllByName} to make code more testable.
- */
-public interface Network {
- Network DEFAULT = new Network() {
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- if (host == null) throw new UnknownHostException("host == null");
- return InetAddress.getAllByName(host);
- }
- };
-
- InetAddress[] resolveInetAddresses(String host) throws UnknownHostException;
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
index b906495..a2df181 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -16,8 +16,13 @@
*/
package com.squareup.okhttp.internal;
+import android.util.Log;
import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.tls.AndroidTrustRootIndex;
+import com.squareup.okhttp.internal.tls.RealTrustRootIndex;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -29,6 +34,8 @@
import java.util.List;
import java.util.logging.Level;
import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
import okio.Buffer;
import static com.squareup.okhttp.internal.Internal.logger;
@@ -50,6 +57,11 @@
* unstable.
*
* Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
+ *
+ * <h3>Trust Manager Extraction</h3>
+ *
+ * <p>Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover the trust
+ * manager that was used to create an {@link SSLSocketFactory}.
*/
public class Platform {
private static final Platform PLATFORM = findPlatform();
@@ -73,6 +85,14 @@
public void untagSocket(Socket socket) throws SocketException {
}
+ public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ return null;
+ }
+
+ public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+ return new RealTrustRootIndex(trustManager.getAcceptedIssuers());
+ }
+
/**
* Configure TLS extensions on {@code sslSocket} for {@code route}.
*
@@ -100,15 +120,21 @@
socket.connect(address, connectTimeout);
}
+ public void log(String message) {
+ System.out.println(message);
+ }
+
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
// Attempt to find Android 2.3+ APIs.
try {
+ Class<?> sslParametersClass;
try {
- Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
+ sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
} catch (ClassNotFoundException e) {
// Older platform before being unbundled.
- Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+ sslParametersClass = Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
}
OptionalMethod<Socket> setUseSessionTickets
@@ -136,25 +162,34 @@
} catch (ClassNotFoundException | NoSuchMethodException ignored) {
}
- return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket,
- trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols);
+ return new Android(sslParametersClass, setUseSessionTickets, setHostname,
+ trafficStatsTagSocket, trafficStatsUntagSocket, getAlpnSelectedProtocol,
+ setAlpnProtocols);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
}
- // Find Jetty's ALPN extension for OpenJDK.
+ // Find an Oracle JDK.
try {
- String negoClassName = "org.eclipse.jetty.alpn.ALPN";
- Class<?> negoClass = Class.forName(negoClassName);
- Class<?> providerClass = Class.forName(negoClassName + "$Provider");
- Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
- Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
- Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
- Method getMethod = negoClass.getMethod("get", SSLSocket.class);
- Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
- return new JdkWithJettyBootPlatform(
- putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
- } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+ Class<?> sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl");
+
+ // Find Jetty's ALPN extension for OpenJDK.
+ try {
+ String negoClassName = "org.eclipse.jetty.alpn.ALPN";
+ Class<?> negoClass = Class.forName(negoClassName);
+ Class<?> providerClass = Class.forName(negoClassName + "$Provider");
+ Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
+ Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
+ Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
+ Method getMethod = negoClass.getMethod("get", SSLSocket.class);
+ Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
+ return new JdkWithJettyBootPlatform(sslContextClass,
+ putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
+ } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+ }
+
+ return new JdkPlatform(sslContextClass);
+ } catch (ClassNotFoundException ignored) {
}
return new Platform();
@@ -162,6 +197,9 @@
/** Android 2.3 or better. */
private static class Android extends Platform {
+ private static final int MAX_LOG_LENGTH = 4000;
+
+ private final Class<?> sslParametersClass;
private final OptionalMethod<Socket> setUseSessionTickets;
private final OptionalMethod<Socket> setHostname;
@@ -173,9 +211,11 @@
private final OptionalMethod<Socket> getAlpnSelectedProtocol;
private final OptionalMethod<Socket> setAlpnProtocols;
- public Android(OptionalMethod<Socket> setUseSessionTickets, OptionalMethod<Socket> setHostname,
- Method trafficStatsTagSocket, Method trafficStatsUntagSocket,
- OptionalMethod<Socket> getAlpnSelectedProtocol, OptionalMethod<Socket> setAlpnProtocols) {
+ public Android(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
+ OptionalMethod<Socket> setHostname, Method trafficStatsTagSocket,
+ Method trafficStatsUntagSocket, OptionalMethod<Socket> getAlpnSelectedProtocol,
+ OptionalMethod<Socket> setAlpnProtocols) {
+ this.sslParametersClass = sslParametersClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
this.trafficStatsTagSocket = trafficStatsTagSocket;
@@ -188,15 +228,46 @@
int connectTimeout) throws IOException {
try {
socket.connect(address, connectTimeout);
- } catch (SecurityException se) {
+ } catch (AssertionError e) {
+ if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+ throw e;
+ } catch (SecurityException e) {
// Before android 4.3, socket.connect could throw a SecurityException
// if opening a socket resulted in an EACCES error.
IOException ioException = new IOException("Exception in connect");
- ioException.initCause(se);
+ ioException.initCause(e);
throw ioException;
}
}
+ @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters");
+ if (context == null) {
+ // If that didn't work, try the Google Play Services SSL provider before giving up. This
+ // must be loaded by the SSLSocketFactory's class loader.
+ try {
+ Class<?> gmsSslParametersClass = Class.forName(
+ "com.google.android.gms.org.conscrypt.SSLParametersImpl", false,
+ sslSocketFactory.getClass().getClassLoader());
+ context = readFieldOrNull(sslSocketFactory, gmsSslParametersClass, "sslParameters");
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ X509TrustManager x509TrustManager = readFieldOrNull(
+ context, X509TrustManager.class, "x509TrustManager");
+ if (x509TrustManager != null) return x509TrustManager;
+
+ return readFieldOrNull(context, X509TrustManager.class, "trustManager");
+ }
+
+ @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+ TrustRootIndex result = AndroidTrustRootIndex.get(trustManager);
+ if (result != null) return result;
+ return super.trustRootIndex(trustManager);
+ }
+
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
// Enable SNI and session tickets.
@@ -243,20 +314,49 @@
throw new RuntimeException(e.getCause());
}
}
+
+ @Override public void log(String message) {
+ // Split by line, then ensure each line can fit into Log's maximum length.
+ for (int i = 0, length = message.length(); i < length; i++) {
+ int newline = message.indexOf('\n', i);
+ newline = newline != -1 ? newline : length;
+ do {
+ int end = Math.min(newline, i + MAX_LOG_LENGTH);
+ Log.d("OkHttp", message.substring(i, end));
+ i = end;
+ } while (i < newline);
+ }
+ }
+ }
+
+ /** JDK 1.7 or better. */
+ private static class JdkPlatform extends Platform {
+ private final Class<?> sslContextClass;
+
+ public JdkPlatform(Class<?> sslContextClass) {
+ this.sslContextClass = sslContextClass;
+ }
+
+ @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context");
+ if (context == null) return null;
+ return readFieldOrNull(context, X509TrustManager.class, "trustManager");
+ }
}
/**
* OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path.
*/
- private static class JdkWithJettyBootPlatform extends Platform {
+ private static class JdkWithJettyBootPlatform extends JdkPlatform {
private final Method putMethod;
private final Method getMethod;
private final Method removeMethod;
private final Class<?> clientProviderClass;
private final Class<?> serverProviderClass;
- public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod,
- Class<?> clientProviderClass, Class<?> serverProviderClass) {
+ public JdkWithJettyBootPlatform(Class<?> sslContextClass, Method putMethod, Method getMethod,
+ Method removeMethod, Class<?> clientProviderClass, Class<?> serverProviderClass) {
+ super(sslContextClass);
this.putMethod = putMethod;
this.getMethod = getMethod;
this.removeMethod = removeMethod;
@@ -368,4 +468,27 @@
}
return result.readByteArray();
}
+
+ static <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) {
+ for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) {
+ try {
+ Field field = c.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object value = field.get(instance);
+ if (value == null || !fieldType.isInstance(value)) return null;
+ return fieldType.cast(value);
+ } catch (NoSuchFieldException ignored) {
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+
+ // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate.
+ if (!fieldName.equals("delegate")) {
+ Object delegate = readFieldOrNull(instance, Object.class, "delegate");
+ if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName);
+ }
+
+ return null;
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
index 4000fbe..b05dc6d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -259,8 +259,8 @@
public static String hostHeader(HttpUrl url) {
// TODO: square braces for IPv6 ?
return url.port() != HttpUrl.defaultPort(url.scheme())
- ? url.rfc2732host() + ":" + url.port()
- : url.rfc2732host();
+ ? url.host() + ":" + url.port()
+ : url.host();
}
/** Returns {@code s} with control characters and non-ASCII characters replaced with '?'. */
@@ -288,4 +288,15 @@
return e.getCause() != null && e.getMessage() != null
&& e.getMessage().contains("getsockname failed");
}
+
+ public static boolean contains(String[] array, String value) {
+ return Arrays.asList(array).contains(value);
+ }
+
+ public static String[] concat(String[] array, String value) {
+ String[] result = new String[array.length + 1];
+ System.arraycopy(array, 0, result, 0, array.length);
+ result[result.length - 1] = value;
+ return result;
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
index a86924b..6feb16c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
@@ -35,6 +35,7 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import okio.Buffer;
+import okio.BufferedSink;
import okio.BufferedSource;
import okio.ByteString;
import okio.Okio;
@@ -76,10 +77,10 @@
final boolean client;
/**
- * User code to run in response to an incoming stream. Callbacks must not be
- * run on the callback executor.
+ * User code to run in response to incoming streams or settings. Calls to this are always invoked
+ * on {@link #executor}.
*/
- private final IncomingStreamHandler handler;
+ private final Listener listener;
private final Map<Integer, FramedStream> streams = new HashMap<>();
private final String hostName;
private int lastGoodStreamId;
@@ -111,9 +112,8 @@
long bytesLeftInWriteWindow;
/** Settings we communicate to the peer. */
- // TODO: Do we want to dynamically adjust settings, or KISS and only set once?
- final Settings okHttpSettings = new Settings();
- // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max);
+ Settings okHttpSettings = new Settings();
+
private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024;
/** Settings we receive from the peer. */
@@ -132,7 +132,7 @@
protocol = builder.protocol;
pushObserver = builder.pushObserver;
client = builder.client;
- handler = builder.handler;
+ listener = builder.listener;
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-5.1.1
nextStreamId = builder.client ? 1 : 2;
if (builder.client && protocol == Protocol.HTTP_2) {
@@ -168,9 +168,9 @@
}
bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
socket = builder.socket;
- frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client);
+ frameWriter = variant.newWriter(builder.sink, client);
- readerRunnable = new Reader();
+ readerRunnable = new Reader(variant.newReader(builder.source, client));
new Thread(readerRunnable).start(); // Not a daemon thread.
}
@@ -209,6 +209,10 @@
return idleStartTimeNs != Long.MAX_VALUE;
}
+ public synchronized int maxConcurrentStreams() {
+ return peerSettings.getMaxConcurrentStreams(Integer.MAX_VALUE);
+ }
+
/**
* Returns the time in ns when this connection became idle or Long.MAX_VALUE
* if connection is not idle.
@@ -515,30 +519,53 @@
}
}
+ /** Merges {@code settings} into this peer's settings and sends them to the remote peer. */
+ public void setSettings(Settings settings) throws IOException {
+ synchronized (frameWriter) {
+ synchronized (this) {
+ if (shutdown) {
+ throw new IOException("shutdown");
+ }
+ okHttpSettings.merge(settings);
+ frameWriter.settings(settings);
+ }
+ }
+ }
+
public static class Builder {
- private String hostName;
private Socket socket;
- private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
+ private String hostName;
+ private BufferedSource source;
+ private BufferedSink sink;
+ private Listener listener = Listener.REFUSE_INCOMING_STREAMS;
private Protocol protocol = Protocol.SPDY_3;
private PushObserver pushObserver = PushObserver.CANCEL;
private boolean client;
- public Builder(boolean client, Socket socket) throws IOException {
- this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket);
- }
-
/**
* @param client true if this peer initiated the connection; false if this
* peer accepted the connection.
*/
- public Builder(String hostName, boolean client, Socket socket) throws IOException {
- this.hostName = hostName;
+ public Builder(boolean client) throws IOException {
this.client = client;
- this.socket = socket;
}
- public Builder handler(IncomingStreamHandler handler) {
- this.handler = handler;
+ public Builder socket(Socket socket) throws IOException {
+ return socket(socket, ((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(),
+ Okio.buffer(Okio.source(socket)), Okio.buffer(Okio.sink(socket)));
+ }
+
+ public Builder socket(
+ Socket socket, String hostName, BufferedSource source, BufferedSink sink) {
+ this.socket = socket;
+ this.hostName = hostName;
+ this.source = source;
+ this.sink = sink;
+ return this;
+ }
+
+ public Builder listener(Listener listener) {
+ this.listener = listener;
return this;
}
@@ -562,17 +589,17 @@
* write a frame, create an async task to do so.
*/
class Reader extends NamedRunnable implements FrameReader.Handler {
- FrameReader frameReader;
+ final FrameReader frameReader;
- private Reader() {
+ private Reader(FrameReader frameReader) {
super("OkHttp %s", hostName);
+ this.frameReader = frameReader;
}
@Override protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
- frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client);
if (!client) {
frameReader.readConnectionPreface();
}
@@ -645,9 +672,9 @@
executor.execute(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) {
@Override public void execute() {
try {
- handler.receive(newStream);
+ listener.onStream(newStream);
} catch (IOException e) {
- logger.log(Level.INFO, "StreamHandler failure for " + hostName, e);
+ logger.log(Level.INFO, "FramedConnection.Listener failure for " + hostName, e);
try {
newStream.close(ErrorCode.PROTOCOL_ERROR);
} catch (IOException ignored) {
@@ -703,6 +730,11 @@
streamsToNotify = streams.values().toArray(new FramedStream[streams.size()]);
}
}
+ executor.execute(new NamedRunnable("OkHttp %s settings", hostName) {
+ @Override public void execute() {
+ listener.onSettings(FramedConnection.this);
+ }
+ });
}
if (streamsToNotify != null && delta != 0) {
for (FramedStream stream : streamsToNotify) {
@@ -878,4 +910,34 @@
}
});
}
+
+ /** Listener of streams and settings initiated by the peer. */
+ public abstract static class Listener {
+ public static final Listener REFUSE_INCOMING_STREAMS = new Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
+ stream.close(ErrorCode.REFUSED_STREAM);
+ }
+ };
+
+ /**
+ * Handle a new stream from this connection's peer. Implementations should
+ * respond by either {@linkplain FramedStream#reply replying to the stream}
+ * or {@linkplain FramedStream#close closing it}. This response does not
+ * need to be synchronous.
+ */
+ public abstract void onStream(FramedStream stream) throws IOException;
+
+ /**
+ * Notification that the connection's peer's settings may have changed.
+ * Implementations should take appropriate action to handle the updated
+ * settings.
+ *
+ * <p>It is the implementation's responsibility to handle concurrent calls
+ * to this method. A remote peer that sends multiple settings frames will
+ * trigger multiple calls to this method, and those calls are not
+ * necessarily serialized.
+ */
+ public void onSettings(FramedConnection connection) {
+ }
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
deleted file mode 100644
index 57863df..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
+++ /dev/null
@@ -1,36 +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.framed;
-
-import java.io.IOException;
-
-/** 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(FramedStream stream) throws IOException {
- stream.close(ErrorCode.REFUSED_STREAM);
- }
- };
-
- /**
- * Handle a new stream from this connection's peer. Implementations should
- * respond by either {@link FramedStream#reply replying to the stream} or
- * {@link FramedStream#close closing it}. This response does not need to be
- * synchronous.
- */
- void receive(FramedStream stream) throws IOException;
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
index a9f9b5a..8d88410 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
@@ -43,7 +43,7 @@
if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue;
PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
- url.rfc2732host(), getConnectToInetAddress(proxy, url), url.port(), url.scheme(),
+ url.host(), getConnectToInetAddress(proxy, url), url.port(), url.scheme(),
challenge.getRealm(), challenge.getScheme(), url.url(), RequestorType.SERVER);
if (auth == null) continue;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
deleted file mode 100644
index abeaf86..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * 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 com.squareup.okhttp.Headers;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.framed.ErrorCode;
-import com.squareup.okhttp.internal.framed.FramedConnection;
-import com.squareup.okhttp.internal.framed.FramedStream;
-import com.squareup.okhttp.internal.framed.Header;
-import java.io.IOException;
-import java.net.ProtocolException;
-import java.util.ArrayList;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.TimeUnit;
-import okio.ByteString;
-import okio.Okio;
-import okio.Sink;
-
-import static com.squareup.okhttp.internal.framed.Header.RESPONSE_STATUS;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_AUTHORITY;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_HOST;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_METHOD;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_PATH;
-import static com.squareup.okhttp.internal.framed.Header.TARGET_SCHEME;
-import static com.squareup.okhttp.internal.framed.Header.VERSION;
-
-public final class FramedTransport implements Transport {
- /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
- private static final List<ByteString> SPDY_3_PROHIBITED_HEADERS = Util.immutableList(
- ByteString.encodeUtf8("connection"),
- ByteString.encodeUtf8("host"),
- ByteString.encodeUtf8("keep-alive"),
- ByteString.encodeUtf8("proxy-connection"),
- ByteString.encodeUtf8("transfer-encoding"));
-
- /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
- private static final List<ByteString> HTTP_2_PROHIBITED_HEADERS = Util.immutableList(
- ByteString.encodeUtf8("connection"),
- ByteString.encodeUtf8("host"),
- ByteString.encodeUtf8("keep-alive"),
- ByteString.encodeUtf8("proxy-connection"),
- ByteString.encodeUtf8("te"),
- ByteString.encodeUtf8("transfer-encoding"),
- ByteString.encodeUtf8("encoding"),
- ByteString.encodeUtf8("upgrade"));
-
- private final HttpEngine httpEngine;
- private final FramedConnection framedConnection;
- private FramedStream stream;
-
- public FramedTransport(HttpEngine httpEngine, FramedConnection framedConnection) {
- this.httpEngine = httpEngine;
- this.framedConnection = framedConnection;
- }
-
- @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
- return stream.getSink();
- }
-
- @Override public void writeRequestHeaders(Request request) throws IOException {
- if (stream != null) return;
-
- httpEngine.writingRequestHeaders();
- boolean permitsRequestBody = httpEngine.permitsRequestBody();
- boolean hasResponseBody = true;
- String version = RequestLine.version(httpEngine.getConnection().getProtocol());
- stream = framedConnection.newStream(
- writeNameValueBlock(request, framedConnection.getProtocol(), version), permitsRequestBody,
- hasResponseBody);
- stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
- }
-
- @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
- requestBody.writeToSocket(stream.getSink());
- }
-
- @Override public void finishRequest() throws IOException {
- stream.getSink().close();
- }
-
- @Override public Response.Builder readResponseHeaders() throws IOException {
- return readNameValueBlock(stream.getResponseHeaders(), framedConnection.getProtocol());
- }
-
- /**
- * Returns a list of alternating names and values containing a SPDY request.
- * Names are all lowercase. No names are repeated. If any name has multiple
- * values, they are concatenated using "\0" as a delimiter.
- */
- public static List<Header> writeNameValueBlock(Request request, Protocol protocol,
- String version) {
- Headers headers = request.headers();
- List<Header> result = new ArrayList<>(headers.size() + 10);
- result.add(new Header(TARGET_METHOD, request.method()));
- result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
- String host = Util.hostHeader(request.httpUrl());
- if (Protocol.SPDY_3 == protocol) {
- result.add(new Header(VERSION, version));
- result.add(new Header(TARGET_HOST, host));
- } else if (Protocol.HTTP_2 == protocol) {
- result.add(new Header(TARGET_AUTHORITY, host)); // Optional in HTTP/2
- } else {
- throw new AssertionError();
- }
- result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
-
- Set<ByteString> names = new LinkedHashSet<ByteString>();
- for (int i = 0, size = headers.size(); i < size; i++) {
- // header names must be lowercase.
- ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
- String value = headers.value(i);
-
- // Drop headers that are forbidden when layering HTTP over SPDY.
- if (isProhibitedHeader(protocol, name)) continue;
-
- // They shouldn't be set, but if they are, drop them. We've already written them!
- if (name.equals(TARGET_METHOD)
- || name.equals(TARGET_PATH)
- || name.equals(TARGET_SCHEME)
- || name.equals(TARGET_AUTHORITY)
- || name.equals(TARGET_HOST)
- || name.equals(VERSION)) {
- continue;
- }
-
- // If we haven't seen this name before, add the pair to the end of the list...
- if (names.add(name)) {
- result.add(new Header(name, value));
- continue;
- }
-
- // ...otherwise concatenate the existing values and this value.
- for (int j = 0; j < result.size(); j++) {
- if (result.get(j).name.equals(name)) {
- String concatenated = joinOnNull(result.get(j).value.utf8(), value);
- result.set(j, new Header(name, concatenated));
- break;
- }
- }
- }
- return result;
- }
-
- private static String joinOnNull(String first, String second) {
- return new StringBuilder(first).append('\0').append(second).toString();
- }
-
- /** Returns headers for a name value block containing a SPDY response. */
- public static Response.Builder readNameValueBlock(List<Header> headerBlock,
- Protocol protocol) throws IOException {
- String status = null;
- String version = "HTTP/1.1"; // :version present only in spdy/3.
-
- Headers.Builder headersBuilder = new Headers.Builder();
- headersBuilder.set(OkHeaders.SELECTED_PROTOCOL, protocol.toString());
- for (int i = 0, size = headerBlock.size(); i < size; i++) {
- ByteString name = headerBlock.get(i).name;
- String values = headerBlock.get(i).value.utf8();
- for (int start = 0; start < values.length(); ) {
- int end = values.indexOf('\0', start);
- if (end == -1) {
- end = values.length();
- }
- String value = values.substring(start, end);
- if (name.equals(RESPONSE_STATUS)) {
- status = value;
- } else if (name.equals(VERSION)) {
- version = value;
- } else if (!isProhibitedHeader(protocol, name)) { // Don't write forbidden headers!
- headersBuilder.add(name.utf8(), value);
- }
- start = end + 1;
- }
- }
- if (status == null) throw new ProtocolException("Expected ':status' header not present");
-
- StatusLine statusLine = StatusLine.parse(version + " " + status);
- return new Response.Builder()
- .protocol(protocol)
- .code(statusLine.code)
- .message(statusLine.message)
- .headers(headersBuilder.build());
- }
-
- @Override public ResponseBody openResponseBody(Response response) throws IOException {
- return new RealResponseBody(response.headers(), Okio.buffer(stream.getSource()));
- }
-
- @Override public void releaseConnectionOnIdle() {
- }
-
- @Override public void disconnect(HttpEngine engine) throws IOException {
- if (stream != null) stream.close(ErrorCode.CANCEL);
- }
-
- @Override public boolean canReuseConnection() {
- return true; // TODO: framedConnection.isClosed() ?
- }
-
- /** When true, this header should not be emitted or consumed. */
- private static boolean isProhibitedHeader(Protocol protocol, ByteString name) {
- if (protocol == Protocol.SPDY_3) {
- return SPDY_3_PROHIBITED_HEADERS.contains(name);
- } else if (protocol == Protocol.HTTP_2) {
- return HTTP_2_PROHIBITED_HEADERS.contains(name);
- } else {
- throw new AssertionError(protocol);
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
similarity index 74%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
index 1bbde80..b43f0d3 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
@@ -16,17 +16,16 @@
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.RealConnection;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
-import java.net.Socket;
-import java.net.SocketTimeoutException;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
@@ -38,7 +37,6 @@
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
-import static com.squareup.okhttp.internal.http.Transport.DISCARD_STREAM_TIMEOUT_MILLIS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
@@ -60,7 +58,7 @@
* #newFixedLengthSource(long) newFixedLengthSource(0)} and may skip reading and
* closing that source.
*/
-public final class HttpConnection {
+public final class Http1xStream implements HttpStream {
private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
private static final int STATE_OPEN_REQUEST_BODY = 1;
private static final int STATE_WRITING_REQUEST_BODY = 2;
@@ -69,63 +67,89 @@
private static final int STATE_READING_RESPONSE_BODY = 5;
private static final int STATE_CLOSED = 6;
- private static final int ON_IDLE_HOLD = 0;
- private static final int ON_IDLE_POOL = 1;
- private static final int ON_IDLE_CLOSE = 2;
-
- private final ConnectionPool pool;
- private final Connection connection;
- private final Socket socket;
+ /** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
+ private final StreamAllocation streamAllocation;
private final BufferedSource source;
private final BufferedSink sink;
-
+ private HttpEngine httpEngine;
private int state = STATE_IDLE;
- private int onIdle = ON_IDLE_HOLD;
- public HttpConnection(ConnectionPool pool, Connection connection, Socket socket)
- throws IOException {
- this.pool = pool;
- this.connection = connection;
- this.socket = socket;
- this.source = Okio.buffer(Okio.source(socket));
- this.sink = Okio.buffer(Okio.sink(socket));
+ public Http1xStream(StreamAllocation streamAllocation, BufferedSource source, BufferedSink sink) {
+ this.streamAllocation = streamAllocation;
+ this.source = source;
+ this.sink = sink;
}
- public void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) {
- if (readTimeoutMillis != 0) {
- source.timeout().timeout(readTimeoutMillis, MILLISECONDS);
+ @Override public void setHttpEngine(HttpEngine httpEngine) {
+ this.httpEngine = httpEngine;
+ }
+
+ @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
+ if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
+ // Stream a request body of unknown length.
+ return newChunkedSink();
}
- if (writeTimeoutMillis != 0) {
- sink.timeout().timeout(writeTimeoutMillis, MILLISECONDS);
+
+ if (contentLength != -1) {
+ // Stream a request body of a known length.
+ return newFixedLengthSink(contentLength);
}
+
+ throw new IllegalStateException(
+ "Cannot stream a request body without chunked encoding or a known content length!");
+ }
+
+ @Override public void cancel() {
+ RealConnection connection = streamAllocation.connection();
+ if (connection != null) connection.cancel();
}
/**
- * Configure this connection to put itself back into the connection pool when
- * the HTTP response body is exhausted.
+ * 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 poolOnIdle() {
- onIdle = ON_IDLE_POOL;
-
- // If we're already idle, go to the pool immediately.
- if (state == STATE_IDLE) {
- onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
- Internal.instance.recycle(pool, connection);
- }
+ @Override public void writeRequestHeaders(Request request) throws IOException {
+ httpEngine.writingRequestHeaders();
+ String requestLine = RequestLine.get(
+ request, httpEngine.getConnection().getRoute().getProxy().type());
+ writeRequest(request.headers(), requestLine);
}
- /**
- * Configure this connection to close itself when the HTTP response body is
- * exhausted.
- */
- public void closeOnIdle() throws IOException {
- onIdle = ON_IDLE_CLOSE;
+ @Override public Response.Builder readResponseHeaders() throws IOException {
+ return readResponse();
+ }
- // If we're already idle, close immediately.
- if (state == STATE_IDLE) {
- state = STATE_CLOSED;
- connection.getSocket().close();
+ @Override public ResponseBody openResponseBody(Response response) throws IOException {
+ Source source = getTransferStream(response);
+ return new RealResponseBody(response.headers(), Okio.buffer(source));
+ }
+
+ private Source getTransferStream(Response response) throws IOException {
+ if (!HttpEngine.hasBody(response)) {
+ return newFixedLengthSource(0);
}
+
+ if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
+ return newChunkedSource(httpEngine);
+ }
+
+ long contentLength = OkHeaders.contentLength(response);
+ if (contentLength != -1) {
+ return newFixedLengthSource(contentLength);
+ }
+
+ // 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 newUnknownLengthSource();
}
/** Returns true if this connection is closed. */
@@ -133,39 +157,10 @@
return state == STATE_CLOSED;
}
- public void closeIfOwnedBy(Object owner) throws IOException {
- Internal.instance.closeIfOwnedBy(connection, owner);
- }
-
- public void flush() throws IOException {
+ @Override public void finishRequest() throws IOException {
sink.flush();
}
- /** Returns the number of buffered bytes immediately readable. */
- public long bufferSize() {
- return source.buffer().size();
- }
-
- /** Test for a stale socket. */
- public boolean isReadable() {
- try {
- int readTimeout = socket.getSoTimeout();
- try {
- socket.setSoTimeout(1);
- if (source.exhausted()) {
- return false; // Stream is exhausted; socket is closed.
- }
- return true;
- } finally {
- socket.setSoTimeout(readTimeout);
- }
- } catch (SocketTimeoutException ignored) {
- return true; // Read timed out; socket is good.
- } catch (IOException e) {
- return false; // Couldn't read; socket is closed.
- }
- }
-
/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
@@ -193,12 +188,8 @@
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
- .message(statusLine.message);
-
- Headers.Builder headersBuilder = new Headers.Builder();
- readHeaders(headersBuilder);
- headersBuilder.add(OkHeaders.SELECTED_PROTOCOL, statusLine.protocol.toString());
- responseBuilder.headers(headersBuilder.build());
+ .message(statusLine.message)
+ .headers(readHeaders());
if (statusLine.code != HTTP_CONTINUE) {
state = STATE_OPEN_RESPONSE_BODY;
@@ -207,19 +198,20 @@
}
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
- IOException exception = new IOException("unexpected end of stream on " + connection
- + " (recycle count=" + Internal.instance.recycleCount(connection) + ")");
+ IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
exception.initCause(e);
throw exception;
}
}
- /** Reads headers or trailers into {@code builder}. */
- public void readHeaders(Headers.Builder builder) throws IOException {
+ /** Reads headers or trailers. */
+ public Headers readHeaders() throws IOException {
+ Headers.Builder headers = new Headers.Builder();
// parse the result headers until the first blank line
for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
- Internal.instance.addLenient(builder, line);
+ Internal.instance.addLenient(headers, line);
}
+ return headers.build();
}
public Sink newChunkedSink() {
@@ -234,7 +226,7 @@
return new FixedLengthSink(contentLength);
}
- public void writeRequestBody(RetryableSink requestBody) throws IOException {
+ @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READ_RESPONSE_HEADERS;
requestBody.writeToSocket(sink);
@@ -254,18 +246,12 @@
public Source newUnknownLengthSource() throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+ if (streamAllocation == null) throw new IllegalStateException("streamAllocation == null");
state = STATE_READING_RESPONSE_BODY;
+ streamAllocation.noNewStreams();
return new UnknownLengthSource();
}
- public BufferedSink rawSink() {
- return sink;
- }
-
- public BufferedSource rawSource() {
- return source;
- }
-
/**
* Sets the delegate of {@code timeout} to {@link Timeout#NONE} and resets its underlying timeout
* to the default configuration. Use this to avoid unexpected sharing of timeouts between pooled
@@ -366,36 +352,25 @@
* 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 recyclable) throws IOException {
+ protected final void endOfInput() throws IOException {
if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
detachTimeout(timeout);
- state = STATE_IDLE;
- if (recyclable && onIdle == ON_IDLE_POOL) {
- onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
- Internal.instance.recycle(pool, connection);
- } else if (onIdle == ON_IDLE_CLOSE) {
- state = STATE_CLOSED;
- connection.getSocket().close();
+ state = STATE_CLOSED;
+ if (streamAllocation != null) {
+ streamAllocation.streamFinished(Http1xStream.this);
}
}
- /**
- * 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() {
- Util.closeQuietly(connection.getSocket());
+ if (state == STATE_CLOSED) return;
+
state = STATE_CLOSED;
+ if (streamAllocation != null) {
+ streamAllocation.noNewStreams();
+ streamAllocation.streamFinished(Http1xStream.this);
+ }
}
}
@@ -406,7 +381,7 @@
public FixedLengthSource(long length) throws IOException {
bytesRemaining = length;
if (bytesRemaining == 0) {
- endOfInput(true);
+ endOfInput();
}
}
@@ -423,7 +398,7 @@
bytesRemaining -= read;
if (bytesRemaining == 0) {
- endOfInput(true);
+ endOfInput();
}
return read;
}
@@ -487,10 +462,8 @@
}
if (bytesRemainingInChunk == 0L) {
hasMoreChunks = false;
- Headers.Builder trailersBuilder = new Headers.Builder();
- readHeaders(trailersBuilder);
- httpEngine.receiveHeaders(trailersBuilder.build());
- endOfInput(true);
+ httpEngine.receiveHeaders(readHeaders());
+ endOfInput();
}
}
@@ -516,7 +489,7 @@
long read = source.read(sink, byteCount);
if (read == -1) {
inputExhausted = true;
- endOfInput(false);
+ endOfInput();
return -1;
}
return read;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java
new file mode 100644
index 0000000..6b8b68f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java
@@ -0,0 +1,296 @@
+/*
+ * 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 com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.framed.ErrorCode;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.framed.FramedStream;
+import com.squareup.okhttp.internal.framed.Header;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import okio.ByteString;
+import okio.ForwardingSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.framed.Header.RESPONSE_STATUS;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_AUTHORITY;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_HOST;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_METHOD;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_PATH;
+import static com.squareup.okhttp.internal.framed.Header.TARGET_SCHEME;
+import static com.squareup.okhttp.internal.framed.Header.VERSION;
+
+/** An HTTP stream for HTTP/2 and SPDY. */
+public final class Http2xStream implements HttpStream {
+ private static final ByteString CONNECTION = ByteString.encodeUtf8("connection");
+ private static final ByteString HOST = ByteString.encodeUtf8("host");
+ private static final ByteString KEEP_ALIVE = ByteString.encodeUtf8("keep-alive");
+ private static final ByteString PROXY_CONNECTION = ByteString.encodeUtf8("proxy-connection");
+ private static final ByteString TRANSFER_ENCODING = ByteString.encodeUtf8("transfer-encoding");
+ private static final ByteString TE = ByteString.encodeUtf8("te");
+ private static final ByteString ENCODING = ByteString.encodeUtf8("encoding");
+ private static final ByteString UPGRADE = ByteString.encodeUtf8("upgrade");
+
+ /** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
+ private static final List<ByteString> SPDY_3_SKIPPED_REQUEST_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TRANSFER_ENCODING,
+ TARGET_METHOD,
+ TARGET_PATH,
+ TARGET_SCHEME,
+ TARGET_AUTHORITY,
+ TARGET_HOST,
+ VERSION);
+ private static final List<ByteString> SPDY_3_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TRANSFER_ENCODING);
+
+ /** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
+ private static final List<ByteString> HTTP_2_SKIPPED_REQUEST_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TE,
+ TRANSFER_ENCODING,
+ ENCODING,
+ UPGRADE,
+ TARGET_METHOD,
+ TARGET_PATH,
+ TARGET_SCHEME,
+ TARGET_AUTHORITY,
+ TARGET_HOST,
+ VERSION);
+ private static final List<ByteString> HTTP_2_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TE,
+ TRANSFER_ENCODING,
+ ENCODING,
+ UPGRADE);
+
+ private final StreamAllocation streamAllocation;
+ private final FramedConnection framedConnection;
+ private HttpEngine httpEngine;
+ private FramedStream stream;
+
+ public Http2xStream(StreamAllocation streamAllocation, FramedConnection framedConnection) {
+ this.streamAllocation = streamAllocation;
+ this.framedConnection = framedConnection;
+ }
+
+ @Override public void setHttpEngine(HttpEngine httpEngine) {
+ this.httpEngine = httpEngine;
+ }
+
+ @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
+ return stream.getSink();
+ }
+
+ @Override public void writeRequestHeaders(Request request) throws IOException {
+ if (stream != null) return;
+
+ httpEngine.writingRequestHeaders();
+ boolean permitsRequestBody = httpEngine.permitsRequestBody(request);
+ List<Header> requestHeaders = framedConnection.getProtocol() == Protocol.HTTP_2
+ ? http2HeadersList(request)
+ : spdy3HeadersList(request);
+ boolean hasResponseBody = true;
+ stream = framedConnection.newStream(requestHeaders, permitsRequestBody, hasResponseBody);
+ stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
+ stream.writeTimeout().timeout(httpEngine.client.getWriteTimeout(), TimeUnit.MILLISECONDS);
+ }
+
+ @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
+ requestBody.writeToSocket(stream.getSink());
+ }
+
+ @Override public void finishRequest() throws IOException {
+ stream.getSink().close();
+ }
+
+ @Override public Response.Builder readResponseHeaders() throws IOException {
+ return framedConnection.getProtocol() == Protocol.HTTP_2
+ ? readHttp2HeadersList(stream.getResponseHeaders())
+ : readSpdy3HeadersList(stream.getResponseHeaders());
+ }
+
+ /**
+ * Returns a list of alternating names and values containing a SPDY request.
+ * Names are all lowercase. No names are repeated. If any name has multiple
+ * values, they are concatenated using "\0" as a delimiter.
+ */
+ public static List<Header> spdy3HeadersList(Request request) {
+ Headers headers = request.headers();
+ List<Header> result = new ArrayList<>(headers.size() + 5);
+ result.add(new Header(TARGET_METHOD, request.method()));
+ result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
+ result.add(new Header(VERSION, "HTTP/1.1"));
+ result.add(new Header(TARGET_HOST, Util.hostHeader(request.httpUrl())));
+ result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
+
+ Set<ByteString> names = new LinkedHashSet<>();
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ // header names must be lowercase.
+ ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
+
+ // Drop headers that are forbidden when layering HTTP over SPDY.
+ if (SPDY_3_SKIPPED_REQUEST_HEADERS.contains(name)) continue;
+
+ // If we haven't seen this name before, add the pair to the end of the list...
+ String value = headers.value(i);
+ if (names.add(name)) {
+ result.add(new Header(name, value));
+ continue;
+ }
+
+ // ...otherwise concatenate the existing values and this value.
+ for (int j = 0; j < result.size(); j++) {
+ if (result.get(j).name.equals(name)) {
+ String concatenated = joinOnNull(result.get(j).value.utf8(), value);
+ result.set(j, new Header(name, concatenated));
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ private static String joinOnNull(String first, String second) {
+ return new StringBuilder(first).append('\0').append(second).toString();
+ }
+
+ public static List<Header> http2HeadersList(Request request) {
+ Headers headers = request.headers();
+ List<Header> result = new ArrayList<>(headers.size() + 4);
+ result.add(new Header(TARGET_METHOD, request.method()));
+ result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
+ result.add(new Header(TARGET_AUTHORITY, Util.hostHeader(request.httpUrl()))); // Optional.
+ result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
+
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ // header names must be lowercase.
+ ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
+ if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name)) {
+ result.add(new Header(name, headers.value(i)));
+ }
+ }
+ return result;
+ }
+
+ /** Returns headers for a name value block containing a SPDY response. */
+ public static Response.Builder readSpdy3HeadersList(List<Header> headerBlock) throws IOException {
+ String status = null;
+ String version = "HTTP/1.1";
+ Headers.Builder headersBuilder = new Headers.Builder();
+ for (int i = 0, size = headerBlock.size(); i < size; i++) {
+ ByteString name = headerBlock.get(i).name;
+
+ String values = headerBlock.get(i).value.utf8();
+ for (int start = 0; start < values.length(); ) {
+ int end = values.indexOf('\0', start);
+ if (end == -1) {
+ end = values.length();
+ }
+ String value = values.substring(start, end);
+ if (name.equals(RESPONSE_STATUS)) {
+ status = value;
+ } else if (name.equals(VERSION)) {
+ version = value;
+ } else if (!SPDY_3_SKIPPED_RESPONSE_HEADERS.contains(name)) {
+ headersBuilder.add(name.utf8(), value);
+ }
+ start = end + 1;
+ }
+ }
+ if (status == null) throw new ProtocolException("Expected ':status' header not present");
+
+ StatusLine statusLine = StatusLine.parse(version + " " + status);
+ return new Response.Builder()
+ .protocol(Protocol.SPDY_3)
+ .code(statusLine.code)
+ .message(statusLine.message)
+ .headers(headersBuilder.build());
+ }
+
+ /** Returns headers for a name value block containing an HTTP/2 response. */
+ public static Response.Builder readHttp2HeadersList(List<Header> headerBlock) throws IOException {
+ String status = null;
+
+ Headers.Builder headersBuilder = new Headers.Builder();
+ for (int i = 0, size = headerBlock.size(); i < size; i++) {
+ ByteString name = headerBlock.get(i).name;
+
+ String value = headerBlock.get(i).value.utf8();
+ if (name.equals(RESPONSE_STATUS)) {
+ status = value;
+ } else if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(name)) {
+ headersBuilder.add(name.utf8(), value);
+ }
+ }
+ if (status == null) throw new ProtocolException("Expected ':status' header not present");
+
+ StatusLine statusLine = StatusLine.parse("HTTP/1.1 " + status);
+ return new Response.Builder()
+ .protocol(Protocol.HTTP_2)
+ .code(statusLine.code)
+ .message(statusLine.message)
+ .headers(headersBuilder.build());
+ }
+
+ @Override public ResponseBody openResponseBody(Response response) throws IOException {
+ Source source = new StreamFinishingSource(stream.getSource());
+ return new RealResponseBody(response.headers(), Okio.buffer(source));
+ }
+
+ @Override public void cancel() {
+ if (stream != null) stream.closeLater(ErrorCode.CANCEL);
+ }
+
+ class StreamFinishingSource extends ForwardingSource {
+ public StreamFinishingSource(Source delegate) {
+ super(delegate);
+ }
+
+ @Override public void close() throws IOException {
+ streamAllocation.streamFinished(Http2xStream.this);
+ super.close();
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 7286428..b80f7d5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -20,7 +20,6 @@
import com.squareup.okhttp.Address;
import com.squareup.okhttp.CertificatePinner;
import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.Interceptor;
@@ -36,18 +35,13 @@
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.Version;
import java.io.IOException;
-import java.io.InterruptedIOException;
import java.net.CookieHandler;
import java.net.ProtocolException;
import java.net.Proxy;
-import java.net.SocketTimeoutException;
-import java.security.cert.CertificateException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
import okio.Buffer;
import okio.BufferedSink;
@@ -111,13 +105,9 @@
final OkHttpClient client;
- private Connection connection;
- private Address address;
- private RouteSelector routeSelector;
- private Route route;
+ public final StreamAllocation streamAllocation;
private final Response priorResponse;
-
- private Transport transport;
+ private HttpStream httpStream;
/** The time when the request headers were written, or -1 if they haven't been written yet. */
long sentRequestMillis = -1;
@@ -178,30 +168,20 @@
* @param callerWritesRequestBody true for the {@code HttpURLConnection}-style interaction
* model where control flow is returned to the calling application to write the request body
* before the response body is readable.
- * @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.
- * @param routeSelector the route selector used for a failed attempt immediately preceding this
*/
public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,
- boolean callerWritesRequestBody, boolean forWebSocket, Connection connection,
- RouteSelector routeSelector, RetryableSink requestBodyOut, Response priorResponse) {
+ boolean callerWritesRequestBody, boolean forWebSocket, StreamAllocation streamAllocation,
+ RetryableSink requestBodyOut, Response priorResponse) {
this.client = client;
this.userRequest = request;
this.bufferRequestBody = bufferRequestBody;
this.callerWritesRequestBody = callerWritesRequestBody;
this.forWebSocket = forWebSocket;
- this.connection = connection;
- this.routeSelector = routeSelector;
+ this.streamAllocation = streamAllocation != null
+ ? streamAllocation
+ : new StreamAllocation(client.getConnectionPool(), createAddress(client, request));
this.requestBodyOut = requestBodyOut;
this.priorResponse = priorResponse;
-
- if (connection != null) {
- Internal.instance.setOwner(connection, this);
- this.route = connection.getRoute();
- } else {
- this.route = null;
- }
}
/**
@@ -218,7 +198,7 @@
*/
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
- if (transport != null) throw new IllegalStateException();
+ if (httpStream != null) throw new IllegalStateException();
Request request = networkRequest(userRequest);
@@ -241,18 +221,14 @@
}
if (networkRequest != null) {
- // Open a connection unless we inherited one from a redirect.
- if (connection == null) {
- connect();
- }
-
- transport = Internal.instance.newTransport(connection, this);
+ httpStream = connect();
+ httpStream.setHttpEngine(this);
// If the caller's control flow writes the request body, we need to create that stream
// immediately. And that means we need to immediately write the request headers, so we can
// start streaming the request body. (We may already have a request body if we're retrying a
// failed POST.)
- if (callerWritesRequestBody && permitsRequestBody() && requestBodyOut == null) {
+ if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) {
long contentLength = OkHeaders.contentLength(request);
if (bufferRequestBody) {
if (contentLength > Integer.MAX_VALUE) {
@@ -262,7 +238,7 @@
if (contentLength != -1) {
// Buffer a request body of a known length.
- transport.writeRequestHeaders(networkRequest);
+ httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = new RetryableSink((int) contentLength);
} else {
// Buffer a request body of an unknown length. Don't write request
@@ -271,18 +247,12 @@
requestBodyOut = new RetryableSink();
}
} else {
- transport.writeRequestHeaders(networkRequest);
- requestBodyOut = transport.createRequestBody(networkRequest, contentLength);
+ httpStream.writeRequestHeaders(networkRequest);
+ requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
}
}
} else {
- // We aren't using the network. Recycle a connection we may have inherited from a redirect.
- if (connection != null) {
- Internal.instance.recycle(client.getConnectionPool(), connection);
- connection = null;
- }
-
if (cacheResponse != null) {
// We have a valid cached response. Promote it to the user response immediately.
this.userResponse = cacheResponse.newBuilder()
@@ -306,48 +276,19 @@
}
}
+ private HttpStream connect() throws RouteException, RequestException, IOException {
+ boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET");
+ return streamAllocation.newStream(client.getConnectTimeout(),
+ client.getReadTimeout(), client.getWriteTimeout(),
+ client.getRetryOnConnectionFailure(), doExtensiveHealthChecks);
+ }
+
private static Response stripBody(Response response) {
return response != null && response.body() != null
? response.newBuilder().body(null).build()
: response;
}
- /** Connect to the origin server either directly or via a proxy. */
- private void connect() throws RequestException, RouteException {
- if (connection != null) throw new IllegalStateException();
-
- if (routeSelector == null) {
- address = createAddress(client, networkRequest);
- try {
- routeSelector = RouteSelector.get(address, networkRequest, client);
- } catch (IOException e) {
- throw new RequestException(e);
- }
- }
-
- connection = createNextConnection();
- Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
- route = connection.getRoute();
- }
-
- private Connection createNextConnection() throws RouteException {
- ConnectionPool pool = client.getConnectionPool();
-
- // Always prefer pooled connections over new connections.
- for (Connection pooled; (pooled = pool.get(address)) != null; ) {
- if (networkRequest.method().equals("GET") || Internal.instance.isReadable(pooled)) {
- return pooled;
- }
- closeQuietly(pooled.getSocket());
- }
-
- try {
- Route route = routeSelector.next();
- return new Connection(pool, route);
- } catch (IOException e) {
- throw new RouteException(e);
- }
- }
/**
* Called immediately before the transport transmits HTTP request headers.
@@ -358,8 +299,8 @@
sentRequestMillis = System.currentTimeMillis();
}
- boolean permitsRequestBody() {
- return HttpMethod.permitsRequestBody(userRequest.method());
+ boolean permitsRequestBody(Request request) {
+ return HttpMethod.permitsRequestBody(request.method());
}
/** Returns the request body or null if this request doesn't have a body. */
@@ -393,7 +334,7 @@
}
public Connection getConnection() {
- return connection;
+ return streamAllocation.connection();
}
/**
@@ -402,64 +343,19 @@
* there are no more routes to try.
*/
public HttpEngine recover(RouteException e) {
- if (routeSelector != null && connection != null) {
- connectFailed(routeSelector, e.getLastConnectException());
- }
-
- if (routeSelector == null && connection == null // No connection.
- || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
- || !isRecoverable(e)) {
+ if (!streamAllocation.recover(e)) {
return null;
}
- Connection connection = close();
+ if (!client.getRetryOnConnectionFailure()) {
+ return null;
+ }
+
+ StreamAllocation streamAllocation = close();
// For failure recovery, use the same route selector with a new connection.
return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody,
- forWebSocket, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse);
- }
-
- private boolean isRecoverable(RouteException e) {
- // If the application has opted-out of recovery, don't recover.
- if (!client.getRetryOnConnectionFailure()) {
- return false;
- }
-
- // Problems with a route may mean the connection can be retried with a new route, or may
- // indicate a client-side or server-side issue that should not be retried. To tell, we must look
- // at the cause.
-
- IOException ioe = e.getLastConnectException();
-
- // If there was a protocol problem, don't recover.
- if (ioe instanceof ProtocolException) {
- return false;
- }
-
- // If there was an interruption don't recover, but if there was a timeout
- // we should try the next route (if there is one).
- if (ioe instanceof InterruptedIOException) {
- return ioe instanceof SocketTimeoutException;
- }
-
- // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
- // again with a different route.
- if (ioe instanceof SSLHandshakeException) {
- // If the problem was a CertificateException from the X509TrustManager,
- // do not retry.
- if (ioe.getCause() instanceof CertificateException) {
- return false;
- }
- }
- if (ioe instanceof SSLPeerUnverifiedException) {
- // e.g. a certificate pinning error.
- return false;
- }
-
- // An example of one we might want to retry with a different route is a problem connecting to a
- // proxy and would manifest as a standard IOException. Unless it is one we know we should not
- // retry, we return true and try a new route.
- return true;
+ forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
}
/**
@@ -469,63 +365,25 @@
* body is buffered.
*/
public HttpEngine recover(IOException e, Sink requestBodyOut) {
- if (routeSelector != null && connection != null) {
- connectFailed(routeSelector, e);
- }
-
- boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
- if (routeSelector == null && connection == null // No connection.
- || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
- || !isRecoverable(e)
- || !canRetryRequestBody) {
+ if (!streamAllocation.recover(e, requestBodyOut)) {
return null;
}
- Connection connection = close();
+ if (!client.getRetryOnConnectionFailure()) {
+ return null;
+ }
+
+ StreamAllocation streamAllocation = close();
// For failure recovery, use the same route selector with a new connection.
return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody,
- forWebSocket, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse);
- }
-
- private void connectFailed(RouteSelector routeSelector, IOException e) {
- // If this is a recycled connection, don't count its failure against the route.
- if (Internal.instance.recycleCount(connection) > 0) return;
- Route failedRoute = connection.getRoute();
- routeSelector.connectFailed(failedRoute, e);
+ forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
}
public HttpEngine recover(IOException e) {
return recover(e, requestBodyOut);
}
- private boolean isRecoverable(IOException e) {
- // If the application has opted-out of recovery, don't recover.
- if (!client.getRetryOnConnectionFailure()) {
- return false;
- }
-
- // If there was a protocol problem, don't recover.
- if (e instanceof ProtocolException) {
- return false;
- }
-
- // If there was an interruption or timeout, don't recover.
- if (e instanceof InterruptedIOException) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Returns the route used to retrieve the response. Null if we haven't
- * connected yet, or if no connection was necessary.
- */
- public Route getRoute() {
- return route;
- }
-
private void maybeCache() throws IOException {
InternalCache responseCache = Internal.instance.internalCache(client);
if (responseCache == null) return;
@@ -551,11 +409,8 @@
* either exhausted or closed. If it is unneeded when this is called, it will
* be released immediately.
*/
- public void releaseConnection() throws IOException {
- if (transport != null && connection != null) {
- transport.releaseConnectionOnIdle();
- }
- connection = null;
+ public void releaseStreamAllocation() throws IOException {
+ streamAllocation.release();
}
/**
@@ -567,25 +422,15 @@
* transport layer connection has been established (such as a HTTP/2 stream) that is terminated.
* Otherwise if a socket connection is being established, that is terminated.
*/
- public void disconnect() {
- try {
- if (transport != null) {
- transport.disconnect(this);
- } else {
- Connection connection = this.connection;
- if (connection != null) {
- Internal.instance.closeIfOwnedBy(connection, this);
- }
- }
- } catch (IOException ignored) {
- }
+ public void cancel() {
+ streamAllocation.cancel();
}
/**
- * Release any resources held by this engine. If a connection is still held by
- * this engine, it is returned.
+ * Release any resources held by this engine. Returns the stream allocation held by this engine,
+ * which itself must be used or released.
*/
- public Connection close() {
+ public StreamAllocation close() {
if (bufferedRequestBody != null) {
// This also closes the wrapped requestBodyOut.
closeQuietly(bufferedRequestBody);
@@ -593,31 +438,14 @@
closeQuietly(requestBodyOut);
}
- // If this engine never achieved a response body, its connection cannot be reused.
- if (userResponse == null) {
- if (connection != null) closeQuietly(connection.getSocket()); // TODO: does this break SPDY?
- connection = null;
- return null;
+ if (userResponse != null) {
+ closeQuietly(userResponse.body());
+ } else {
+ // If this engine never achieved a response body, its stream allocation is dead.
+ streamAllocation.connectionFailed();
}
- // Close the response body. This will recycle the connection if it is eligible.
- closeQuietly(userResponse.body());
-
- // Close the connection if it cannot be reused.
- if (transport != null && connection != null && !transport.canReuseConnection()) {
- closeQuietly(connection.getSocket());
- connection = null;
- return null;
- }
-
- // Prevent this engine from disconnecting a connection it no longer owns.
- if (connection != null && !Internal.instance.clearOwner(connection)) {
- connection = null;
- }
-
- Connection result = connection;
- connection = null;
- return result;
+ return streamAllocation;
}
/**
@@ -694,8 +522,7 @@
result.header("Host", Util.hostHeader(request.httpUrl()));
}
- if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0)
- && request.header("Connection") == null) {
+ if (request.header("Connection") == null) {
result.header("Connection", "Keep-Alive");
}
@@ -742,7 +569,7 @@
Response networkResponse;
if (forWebSocket) {
- transport.writeRequestHeaders(networkRequest);
+ httpStream.writeRequestHeaders(networkRequest);
networkResponse = readNetworkResponse();
} else if (!callerWritesRequestBody) {
@@ -763,7 +590,7 @@
.header("Content-Length", Long.toString(contentLength))
.build();
}
- transport.writeRequestHeaders(networkRequest);
+ httpStream.writeRequestHeaders(networkRequest);
}
// Write the request body to the socket.
@@ -775,7 +602,7 @@
requestBodyOut.close();
}
if (requestBodyOut instanceof RetryableSink) {
- transport.writeRequestBody((RetryableSink) requestBodyOut);
+ httpStream.writeRequestBody((RetryableSink) requestBodyOut);
}
}
@@ -795,7 +622,7 @@
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
- releaseConnection();
+ releaseStreamAllocation();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
@@ -833,7 +660,7 @@
}
@Override public Connection connection() {
- return connection;
+ return streamAllocation.connection();
}
@Override public Request request() {
@@ -848,7 +675,7 @@
Address address = connection().getRoute().getAddress();
// Confirm that the interceptor uses the connection we've already prepared.
- if (!request.httpUrl().rfc2732host().equals(address.getRfc2732Host())
+ if (!request.httpUrl().host().equals(address.getUriHost())
|| request.httpUrl().port() != address.getUriPort()) {
throw new IllegalStateException("network interceptor " + caller
+ " must retain the same host and port");
@@ -872,17 +699,21 @@
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
+ if (interceptedResponse == null) {
+ throw new NullPointerException("network interceptor " + interceptor
+ + " returned null");
+ }
return interceptedResponse;
}
- transport.writeRequestHeaders(request);
+ httpStream.writeRequestHeaders(request);
//Update the networkRequest with the possibly updated interceptor request.
networkRequest = request;
- if (permitsRequestBody() && request.body() != null) {
- Sink requestBodyOut = transport.createRequestBody(request, request.body().contentLength());
+ if (permitsRequestBody(request) && request.body() != null) {
+ Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
@@ -901,22 +732,26 @@
}
private Response readNetworkResponse() throws IOException {
- transport.finishRequest();
+ httpStream.finishRequest();
- Response networkResponse = transport.readResponseHeaders()
+ Response networkResponse = httpStream.readResponseHeaders()
.request(networkRequest)
- .handshake(connection.getHandshake())
+ .handshake(streamAllocation.connection().getHandshake())
.header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
.header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
.build();
if (!forWebSocket) {
networkResponse = networkResponse.newBuilder()
- .body(transport.openResponseBody(networkResponse))
+ .body(httpStream.openResponseBody(networkResponse))
.build();
}
- Internal.instance.setProtocol(connection, networkResponse.protocol());
+ if ("close".equalsIgnoreCase(networkResponse.request().header("Connection"))
+ || "close".equalsIgnoreCase(networkResponse.header("Connection"))) {
+ streamAllocation.noNewStreams();
+ }
+
return networkResponse;
}
@@ -969,7 +804,7 @@
@Override public void close() throws IOException {
if (!cacheRequestClosed
- && !Util.discard(this, Transport.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
+ && !Util.discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
@@ -1051,11 +886,16 @@
*/
public Request followUpRequest() throws IOException {
if (userResponse == null) throw new IllegalStateException();
- Proxy selectedProxy = getRoute() != null
- ? getRoute().getProxy()
+ Connection connection = streamAllocation.connection();
+ Route route = connection != null
+ ? connection.getRoute()
+ : null;
+ Proxy selectedProxy = route != null
+ ? route.getProxy()
: client.getProxy();
int responseCode = userResponse.code();
+ final String method = userRequest.method();
switch (responseCode) {
case HTTP_PROXY_AUTH:
if (selectedProxy.type() != Proxy.Type.HTTP) {
@@ -1069,7 +909,7 @@
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
- if (!userRequest.method().equals("GET") && !userRequest.method().equals("HEAD")) {
+ if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
@@ -1093,8 +933,12 @@
// Redirects don't include a request body.
Request.Builder requestBuilder = userRequest.newBuilder();
- if (HttpMethod.permitsRequestBody(userRequest.method())) {
- requestBuilder.method("GET", null);
+ if (HttpMethod.permitsRequestBody(method)) {
+ if (HttpMethod.redirectsToGet(method)) {
+ requestBuilder.method("GET", null);
+ } else {
+ requestBuilder.method(method, null);
+ }
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
@@ -1135,7 +979,7 @@
certificatePinner = client.getCertificatePinner();
}
- return new Address(request.httpUrl().rfc2732host(), request.httpUrl().port(),
+ return new Address(request.httpUrl().host(), request.httpUrl().port(), client.getDns(),
client.getSocketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner,
client.getAuthenticator(), client.getProxy(), client.getProtocols(),
client.getConnectionSpecs(), client.getProxySelector());
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
index b5f2a48..b6bf700 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
@@ -20,18 +20,30 @@
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
- || method.equals("DELETE");
+ || method.equals("DELETE")
+ || method.equals("MOVE"); // WebDAV
}
public static boolean requiresRequestBody(String method) {
return method.equals("POST")
|| method.equals("PUT")
- || method.equals("PATCH");
+ || method.equals("PATCH")
+ || method.equals("PROPPATCH") // WebDAV
+ || method.equals("REPORT"); // CalDAV/CardDAV (defined in WebDAV Versioning)
}
public static boolean permitsRequestBody(String method) {
return requiresRequestBody(method)
- || method.equals("DELETE"); // Permitted as spec is ambiguous.
+ || method.equals("OPTIONS")
+ || method.equals("DELETE") // Permitted as spec is ambiguous.
+ || method.equals("PROPFIND") // (WebDAV) without body: request <allprop/>
+ || method.equals("MKCOL") // (WebDAV) may contain a body, but behaviour is unspecified
+ || method.equals("LOCK"); // (WebDAV) body: create lock, without body: refresh lock
+ }
+
+ public static boolean redirectsToGet(String method) {
+ // All requests but PROPFIND should redirect to a GET request.
+ return !method.equals("PROPFIND");
}
private HttpMethod() {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
similarity index 82%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
index 77f7c9e..ef1deb7 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
@@ -22,7 +22,7 @@
import java.io.IOException;
import okio.Sink;
-public interface Transport {
+public interface HttpStream {
/**
* 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
@@ -51,17 +51,11 @@
/** Returns a stream that reads the response body. */
ResponseBody openResponseBody(Response response) throws IOException;
- /**
- * Configures the response body to pool or close the socket connection when
- * the response body is closed.
- */
- void releaseConnectionOnIdle() throws IOException;
-
- void disconnect(HttpEngine engine) throws IOException;
+ void setHttpEngine(HttpEngine httpEngine);
/**
- * Returns true if the socket connection held by this transport can be reused
- * for a follow-up exchange.
+ * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
+ * That may happen later by the connection pool thread.
*/
- boolean canReuseConnection();
+ void cancel();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
deleted file mode 100644
index d02e1e5..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * 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 com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-import java.io.IOException;
-import okio.Okio;
-import okio.Sink;
-import okio.Source;
-
-public final class HttpTransport implements Transport {
- private final HttpEngine httpEngine;
- private final HttpConnection httpConnection;
-
- public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) {
- this.httpEngine = httpEngine;
- this.httpConnection = httpConnection;
- }
-
- @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
- if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
- // Stream a request body of unknown length.
- return httpConnection.newChunkedSink();
- }
-
- if (contentLength != -1) {
- // Stream a request body of a known length.
- return httpConnection.newFixedLengthSink(contentLength);
- }
-
- throw new IllegalStateException(
- "Cannot stream a request body without chunked encoding or a known content length!");
- }
-
- @Override public void finishRequest() throws IOException {
- httpConnection.flush();
- }
-
- @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
- httpConnection.writeRequestBody(requestBody);
- }
-
- /**
- * 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(Request request) throws IOException {
- httpEngine.writingRequestHeaders();
- String requestLine = RequestLine.get(request,
- httpEngine.getConnection().getRoute().getProxy().type(),
- httpEngine.getConnection().getProtocol());
- httpConnection.writeRequest(request.headers(), requestLine);
- }
-
- @Override public Response.Builder readResponseHeaders() throws IOException {
- return httpConnection.readResponse();
- }
-
- @Override public void releaseConnectionOnIdle() throws IOException {
- if (canReuseConnection()) {
- httpConnection.poolOnIdle();
- } else {
- httpConnection.closeOnIdle();
- }
- }
-
- @Override public boolean canReuseConnection() {
- // If the request specified that the connection shouldn't be reused, don't reuse it.
- if ("close".equalsIgnoreCase(httpEngine.getRequest().header("Connection"))) {
- return false;
- }
-
- // If the response specified that the connection shouldn't be reused, don't reuse it.
- if ("close".equalsIgnoreCase(httpEngine.getResponse().header("Connection"))) {
- return false;
- }
-
- if (httpConnection.isClosed()) {
- return false;
- }
-
- return true;
- }
-
- @Override public ResponseBody openResponseBody(Response response) throws IOException {
- Source source = getTransferStream(response);
- return new RealResponseBody(response.headers(), Okio.buffer(source));
- }
-
- private Source getTransferStream(Response response) throws IOException {
- if (!HttpEngine.hasBody(response)) {
- return httpConnection.newFixedLengthSource(0);
- }
-
- if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
- return httpConnection.newChunkedSource(httpEngine);
- }
-
- long contentLength = OkHeaders.contentLength(response);
- if (contentLength != -1) {
- return httpConnection.newFixedLengthSource(contentLength);
- }
-
- // 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 httpConnection.newUnknownLengthSource();
- }
-
- @Override public void disconnect(HttpEngine engine) throws IOException {
- httpConnection.closeIfOwnedBy(engine);
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
index c381c47..5e41c85 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
@@ -55,6 +55,9 @@
*/
public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";
+ /** Synthetic response header: the location from which the response was loaded. */
+ public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
+
private OkHeaders() {
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
index 89a3922..39419a8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
@@ -1,7 +1,6 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.HttpUrl;
-import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import java.net.HttpURLConnection;
import java.net.Proxy;
@@ -15,7 +14,7 @@
* to the application by {@link HttpURLConnection#getHeaderFields}, so it
* needs to be set even if the transport is SPDY.
*/
- static String get(Request request, Proxy.Type proxyType, Protocol protocol) {
+ static String get(Request request, Proxy.Type proxyType) {
StringBuilder result = new StringBuilder();
result.append(request.method());
result.append(' ');
@@ -26,8 +25,7 @@
result.append(requestPath(request.httpUrl()));
}
- result.append(' ');
- result.append(version(protocol));
+ result.append(" HTTP/1.1");
return result.toString();
}
@@ -56,8 +54,4 @@
String query = url.encodedQuery();
return query != null ? (path + '?' + query) : path;
}
-
- public static String version(Protocol protocol) {
- return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
- }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index b210c56..3365914 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -17,11 +17,7 @@
import com.squareup.okhttp.Address;
import com.squareup.okhttp.HttpUrl;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.Network;
import com.squareup.okhttp.internal.RouteDatabase;
import java.io.IOException;
import java.net.InetAddress;
@@ -41,9 +37,6 @@
*/
public final class RouteSelector {
private final Address address;
- private final HttpUrl url;
- private final Network network;
- private final OkHttpClient client;
private final RouteDatabase routeDatabase;
/* The most recently attempted route. */
@@ -61,19 +54,11 @@
/* State for negotiating failed routes */
private final List<Route> postponedRoutes = new ArrayList<>();
- private RouteSelector(Address address, HttpUrl url, OkHttpClient client) {
+ public RouteSelector(Address address, RouteDatabase routeDatabase) {
this.address = address;
- this.url = url;
- this.client = client;
- this.routeDatabase = Internal.instance.routeDatabase(client);
- this.network = Internal.instance.network(client);
+ this.routeDatabase = routeDatabase;
- resetNextProxy(url, address.getProxy());
- }
-
- public static RouteSelector get(Address address, Request request, OkHttpClient client)
- throws IOException {
- return new RouteSelector(address, request.httpUrl(), client);
+ resetNextProxy(address.url(), address.getProxy());
}
/**
@@ -117,7 +102,7 @@
if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && address.getProxySelector() != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
address.getProxySelector().connectFailed(
- url.uri(), failedRoute.getProxy().address(), failure);
+ address.url().uri(), failedRoute.getProxy().address(), failure);
}
routeDatabase.failed(failedRoute);
@@ -132,7 +117,7 @@
// Try each of the ProxySelector choices until one connection succeeds. If none succeed
// then we'll try a direct connection below.
proxies = new ArrayList<>();
- List<Proxy> selectedProxies = client.getProxySelector().select(url.uri());
+ List<Proxy> selectedProxies = address.getProxySelector().select(url.uri());
if (selectedProxies != null) proxies.addAll(selectedProxies);
// Finally try a direct connection. We only try it once!
proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));
@@ -149,7 +134,7 @@
/** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
private Proxy nextProxy() throws IOException {
if (!hasNextProxy()) {
- throw new SocketException("No route to " + address.getRfc2732Host()
+ throw new SocketException("No route to " + address.getUriHost()
+ "; exhausted proxy configurations: " + proxies);
}
Proxy result = proxies.get(nextProxyIndex++);
@@ -165,7 +150,7 @@
String socketHost;
int socketPort;
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
- socketHost = address.getRfc2732Host();
+ socketHost = address.getUriHost();
socketPort = address.getUriPort();
} else {
SocketAddress proxyAddress = proxy.address();
@@ -183,9 +168,15 @@
+ "; port is out of range");
}
- // Try each address for best behavior in mixed IPv4/IPv6 environments.
- for (InetAddress inetAddress : network.resolveInetAddresses(socketHost)) {
- inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+ if (proxy.type() == Proxy.Type.SOCKS) {
+ inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
+ } else {
+ // Try each address for best behavior in mixed IPv4/IPv6 environments.
+ List<InetAddress> addresses = address.getDns().lookup(socketHost);
+ for (int i = 0, size = addresses.size(); i < size; i++) {
+ InetAddress inetAddress = addresses.get(i);
+ inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+ }
}
nextInetSocketAddressIndex = 0;
@@ -217,7 +208,7 @@
/** Returns the next socket address to try. */
private InetSocketAddress nextInetSocketAddress() throws IOException {
if (!hasNextInetSocketAddress()) {
- throw new SocketException("No route to " + address.getRfc2732Host()
+ throw new SocketException("No route to " + address.getUriHost()
+ "; exhausted inet socket addresses: " + inetSocketAddresses);
}
return inetSocketAddresses.get(nextInetSocketAddressIndex++);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java
new file mode 100644
index 0000000..7d95338
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RouteDatabase;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.RealConnection;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.net.ProtocolException;
+import java.net.SocketTimeoutException;
+import java.security.cert.CertificateException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import okio.Sink;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * This class coordinates the relationship between three entities:
+ *
+ * <ul>
+ * <li><strong>Connections:</strong> physical socket connections to remote servers. These are
+ * potentially slow to establish so it is necessary to be able to cancel a connection
+ * currently being connected.
+ * <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
+ * connections. Each connection has its own allocation limit, which defines how many
+ * concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
+ * at a time, SPDY and HTTP/2 typically carry multiple.
+ * <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
+ * its follow up requests. We prefer to keep all streams of a single call on the same
+ * connection for better behavior and locality.
+ * </ul>
+ *
+ * <p>Instances of this class act on behalf of the call, using one or more streams over one or
+ * more connections. This class has APIs to release each of the above resources:
+ *
+ * <ul>
+ * <li>{@link #noNewStreams()} prevents the connection from being used for new streams in the
+ * future. Use this after a {@code Connection: close} header, or when the connection may be
+ * inconsistent.
+ * <li>{@link #streamFinished streamFinished()} releases the active stream from this allocation.
+ * Note that only one stream may be active at a given time, so it is necessary to call {@link
+ * #streamFinished streamFinished()} before creating a subsequent stream with {@link
+ * #newStream newStream()}.
+ * <li>{@link #release()} removes the call's hold on the connection. Note that this won't
+ * immediately free the connection if there is a stream still lingering. That happens when a
+ * call is complete but its response body has yet to be fully consumed.
+ * </ul>
+ *
+ * <p>This class supports {@linkplain #cancel asynchronous canceling}. This is intended to have
+ * the smallest blast radius possible. If an HTTP/2 stream is active, canceling will cancel that
+ * stream but not the other streams sharing its connection. But if the TLS handshake is still in
+ * progress then canceling may break the entire connection.
+ */
+public final class StreamAllocation {
+ public final Address address;
+ private final ConnectionPool connectionPool;
+
+ // State guarded by connectionPool.
+ private RouteSelector routeSelector;
+ private RealConnection connection;
+ private boolean released;
+ private boolean canceled;
+ private HttpStream stream;
+
+ public StreamAllocation(ConnectionPool connectionPool, Address address) {
+ this.connectionPool = connectionPool;
+ this.address = address;
+ }
+
+ public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout,
+ boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
+ throws RouteException, IOException {
+ try {
+ RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
+ writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
+
+ HttpStream resultStream;
+ if (resultConnection.framedConnection != null) {
+ resultStream = new Http2xStream(this, resultConnection.framedConnection);
+ } else {
+ resultConnection.getSocket().setSoTimeout(readTimeout);
+ resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
+ resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
+ resultStream = new Http1xStream(this, resultConnection.source, resultConnection.sink);
+ }
+
+ synchronized (connectionPool) {
+ resultConnection.streamCount++;
+ stream = resultStream;
+ return resultStream;
+ }
+ } catch (IOException e) {
+ throw new RouteException(e);
+ }
+ }
+
+ /**
+ * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
+ * until a healthy connection is found.
+ */
+ private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
+ int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
+ throws IOException, RouteException {
+ while (true) {
+ RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
+ connectionRetryEnabled);
+
+ // If this is a brand new connection, we can skip the extensive health checks.
+ synchronized (connectionPool) {
+ if (candidate.streamCount == 0) {
+ return candidate;
+ }
+ }
+
+ // Otherwise do a potentially-slow check to confirm that the pooled connection is still good.
+ if (candidate.isHealthy(doExtensiveHealthChecks)) {
+ return candidate;
+ }
+
+ connectionFailed();
+ }
+ }
+
+ /**
+ * Returns a connection to host a new stream. This prefers the existing connection if it exists,
+ * then the pool, finally building a new connection.
+ */
+ private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
+ boolean connectionRetryEnabled) throws IOException, RouteException {
+ synchronized (connectionPool) {
+ if (released) throw new IllegalStateException("released");
+ if (stream != null) throw new IllegalStateException("stream != null");
+ if (canceled) throw new IOException("Canceled");
+
+ RealConnection allocatedConnection = this.connection;
+ if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
+ return allocatedConnection;
+ }
+
+ // Attempt to get a connection from the pool.
+ RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
+ if (pooledConnection != null) {
+ this.connection = pooledConnection;
+ return pooledConnection;
+ }
+
+ // Attempt to create a connection.
+ if (routeSelector == null) {
+ routeSelector = new RouteSelector(address, routeDatabase());
+ }
+ }
+
+ Route route = routeSelector.next();
+ RealConnection newConnection = new RealConnection(route);
+ acquire(newConnection);
+
+ synchronized (connectionPool) {
+ Internal.instance.put(connectionPool, newConnection);
+ this.connection = newConnection;
+ if (canceled) throw new IOException("Canceled");
+ }
+
+ newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(),
+ connectionRetryEnabled);
+ routeDatabase().connected(newConnection.getRoute());
+
+ return newConnection;
+ }
+
+ public void streamFinished(HttpStream stream) {
+ synchronized (connectionPool) {
+ if (stream == null || stream != this.stream) {
+ throw new IllegalStateException("expected " + this.stream + " but was " + stream);
+ }
+ }
+ deallocate(false, false, true);
+ }
+
+ public HttpStream stream() {
+ synchronized (connectionPool) {
+ return stream;
+ }
+ }
+
+ private RouteDatabase routeDatabase() {
+ return Internal.instance.routeDatabase(connectionPool);
+ }
+
+ public synchronized RealConnection connection() {
+ return connection;
+ }
+
+ public void release() {
+ deallocate(false, true, false);
+ }
+
+ /** Forbid new streams from being created on the connection that hosts this allocation. */
+ public void noNewStreams() {
+ deallocate(true, false, false);
+ }
+
+ /**
+ * Releases resources held by this allocation. If sufficient resources are allocated, the
+ * connection will be detached or closed.
+ */
+ private void deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
+ RealConnection connectionToClose = null;
+ synchronized (connectionPool) {
+ if (streamFinished) {
+ this.stream = null;
+ }
+ if (released) {
+ this.released = true;
+ }
+ if (connection != null) {
+ if (noNewStreams) {
+ connection.noNewStreams = true;
+ }
+ if (this.stream == null && (this.released || connection.noNewStreams)) {
+ release(connection);
+ if (connection.streamCount > 0) {
+ routeSelector = null;
+ }
+ if (connection.allocations.isEmpty()) {
+ connection.idleAtNanos = System.nanoTime();
+ if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
+ connectionToClose = connection;
+ }
+ }
+ connection = null;
+ }
+ }
+ }
+ if (connectionToClose != null) {
+ Util.closeQuietly(connectionToClose.getSocket());
+ }
+ }
+
+ public void cancel() {
+ HttpStream streamToCancel;
+ RealConnection connectionToCancel;
+ synchronized (connectionPool) {
+ canceled = true;
+ streamToCancel = stream;
+ connectionToCancel = connection;
+ }
+ if (streamToCancel != null) {
+ streamToCancel.cancel();
+ } else if (connectionToCancel != null) {
+ connectionToCancel.cancel();
+ }
+ }
+
+ private void connectionFailed(IOException e) {
+ synchronized (connectionPool) {
+ if (routeSelector != null) {
+ if (connection.streamCount == 0) {
+ // Record the failure on a fresh route.
+ Route failedRoute = connection.getRoute();
+ routeSelector.connectFailed(failedRoute, e);
+ } else {
+ // We saw a failure on a recycled connection, reset this allocation with a fresh route.
+ routeSelector = null;
+ }
+ }
+ }
+ connectionFailed();
+ }
+
+ /** Finish the current stream and prevent new streams from being created. */
+ public void connectionFailed() {
+ deallocate(true, false, true);
+ }
+
+ /**
+ * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
+ * {@link #release} on the same connection.
+ */
+ public void acquire(RealConnection connection) {
+ connection.allocations.add(new WeakReference<>(this));
+ }
+
+ /** Remove this allocation from the connection's list of allocations. */
+ private void release(RealConnection connection) {
+ for (int i = 0, size = connection.allocations.size(); i < size; i++) {
+ Reference<StreamAllocation> reference = connection.allocations.get(i);
+ if (reference.get() == this) {
+ connection.allocations.remove(i);
+ return;
+ }
+ }
+ throw new IllegalStateException();
+ }
+
+ public boolean recover(RouteException e) {
+ if (connection != null) {
+ connectionFailed(e.getLastConnectException());
+ }
+
+ if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt.
+ || !isRecoverable(e)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean recover(IOException e, Sink requestBodyOut) {
+ if (connection != null) {
+ int streamCount = connection.streamCount;
+ connectionFailed(e);
+
+ if (streamCount == 1) {
+ // This isn't a recycled connection.
+ // TODO(jwilson): find a better way for this.
+ return false;
+ }
+ }
+
+ boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
+ if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt.
+ || !isRecoverable(e)
+ || !canRetryRequestBody) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isRecoverable(IOException e) {
+ // If there was a protocol problem, don't recover.
+ if (e instanceof ProtocolException) {
+ return false;
+ }
+
+ // If there was an interruption or timeout, don't recover.
+ if (e instanceof InterruptedIOException) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isRecoverable(RouteException e) {
+ // Problems with a route may mean the connection can be retried with a new route, or may
+ // indicate a client-side or server-side issue that should not be retried. To tell, we must look
+ // at the cause.
+
+ IOException ioe = e.getLastConnectException();
+
+ // If there was a protocol problem, don't recover.
+ if (ioe instanceof ProtocolException) {
+ return false;
+ }
+
+ // If there was an interruption don't recover, but if there was a timeout
+ // we should try the next route (if there is one).
+ if (ioe instanceof InterruptedIOException) {
+ return ioe instanceof SocketTimeoutException;
+ }
+
+ // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
+ // again with a different route.
+ if (ioe instanceof SSLHandshakeException) {
+ // If the problem was a CertificateException from the X509TrustManager,
+ // do not retry.
+ if (ioe.getCause() instanceof CertificateException) {
+ return false;
+ }
+ }
+ if (ioe instanceof SSLPeerUnverifiedException) {
+ // e.g. a certificate pinning error.
+ return false;
+ }
+
+ // An example of one we might want to retry with a different route is a problem connecting to a
+ // proxy and would manifest as a standard IOException. Unless it is one we know we should not
+ // retry, we return true and try a new route.
+ return true;
+ }
+
+ @Override public String toString() {
+ return address.toString();
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java
new file mode 100644
index 0000000..9ff53c1
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java
@@ -0,0 +1,407 @@
+/*
+ * 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.internal.io;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ConnectionSpec;
+import com.squareup.okhttp.Handshake;
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.ConnectionSpecSelector;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.Version;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.http.Http1xStream;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.RouteException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.tls.CertificateChainCleaner;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.net.ConnectException;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownServiceException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.Util.closeQuietly;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+public final class RealConnection implements Connection {
+ private final Route route;
+
+ /** The low-level TCP socket. */
+ private Socket rawSocket;
+
+ /**
+ * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
+ * {@link #rawSocket} itself if this connection does not use SSL.
+ */
+ public Socket socket;
+ private Handshake handshake;
+ private Protocol protocol;
+ public volatile FramedConnection framedConnection;
+ public int streamCount;
+ public BufferedSource source;
+ public BufferedSink sink;
+ public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
+ public boolean noNewStreams;
+ public long idleAtNanos = Long.MAX_VALUE;
+
+ public RealConnection(Route route) {
+ this.route = route;
+ }
+
+ public void connect(int connectTimeout, int readTimeout, int writeTimeout,
+ List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
+ if (protocol != null) throw new IllegalStateException("already connected");
+
+ RouteException routeException = null;
+ ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
+ Proxy proxy = route.getProxy();
+ Address address = route.getAddress();
+
+ if (route.getAddress().getSslSocketFactory() == null
+ && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
+ throw new RouteException(new UnknownServiceException(
+ "CLEARTEXT communication not supported: " + connectionSpecs));
+ }
+
+ while (protocol == null) {
+ try {
+ rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
+ ? address.getSocketFactory().createSocket()
+ : new Socket(proxy);
+ connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
+ } catch (IOException e) {
+ Util.closeQuietly(socket);
+ Util.closeQuietly(rawSocket);
+ socket = null;
+ rawSocket = null;
+ source = null;
+ sink = null;
+ handshake = null;
+ protocol = null;
+
+ if (routeException == null) {
+ routeException = new RouteException(e);
+ } else {
+ routeException.addConnectException(e);
+ }
+
+ if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
+ throw routeException;
+ }
+ }
+ }
+ }
+
+ /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
+ private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
+ ConnectionSpecSelector connectionSpecSelector) throws IOException {
+ rawSocket.setSoTimeout(readTimeout);
+ try {
+ Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);
+ } catch (ConnectException e) {
+ throw new ConnectException("Failed to connect to " + route.getSocketAddress());
+ }
+ source = Okio.buffer(Okio.source(rawSocket));
+ sink = Okio.buffer(Okio.sink(rawSocket));
+
+ if (route.getAddress().getSslSocketFactory() != null) {
+ connectTls(readTimeout, writeTimeout, connectionSpecSelector);
+ } else {
+ protocol = Protocol.HTTP_1_1;
+ socket = rawSocket;
+ }
+
+ if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
+ socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
+
+ FramedConnection framedConnection = new FramedConnection.Builder(true)
+ .socket(socket, route.getAddress().url().host(), source, sink)
+ .protocol(protocol)
+ .build();
+ framedConnection.sendConnectionPreface();
+
+ // Only assign the framed connection once the preface has been sent successfully.
+ this.framedConnection = framedConnection;
+ }
+ }
+
+ private void connectTls(int readTimeout, int writeTimeout,
+ ConnectionSpecSelector connectionSpecSelector) throws IOException {
+ if (route.requiresTunnel()) {
+ createTunnel(readTimeout, writeTimeout);
+ }
+
+ Address address = route.getAddress();
+ SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
+ boolean success = false;
+ SSLSocket sslSocket = null;
+ try {
+ // Create the wrapper over the connected socket.
+ sslSocket = (SSLSocket) sslSocketFactory.createSocket(
+ rawSocket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
+
+ // Configure the socket's ciphers, TLS versions, and extensions.
+ ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
+ if (connectionSpec.supportsTlsExtensions()) {
+ Platform.get().configureTlsExtensions(
+ sslSocket, address.getUriHost(), address.getProtocols());
+ }
+
+ // Force handshake. This can throw!
+ sslSocket.startHandshake();
+ Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
+
+ // Verify that the socket's certificates are acceptable for the target host.
+ if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
+ X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
+ throw new SSLPeerUnverifiedException("Hostname " + address.getUriHost() + " not verified:"
+ + "\n certificate: " + CertificatePinner.pin(cert)
+ + "\n DN: " + cert.getSubjectDN().getName()
+ + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
+ }
+
+ // Check that the certificate pinner is satisfied by the certificates presented.
+ if (address.getCertificatePinner() != CertificatePinner.DEFAULT) {
+ TrustRootIndex trustRootIndex = trustRootIndex(address.getSslSocketFactory());
+ List<Certificate> certificates = new CertificateChainCleaner(trustRootIndex)
+ .clean(unverifiedHandshake.peerCertificates());
+ address.getCertificatePinner().check(address.getUriHost(), certificates);
+ }
+
+ // Success! Save the handshake and the ALPN protocol.
+ String maybeProtocol = connectionSpec.supportsTlsExtensions()
+ ? Platform.get().getSelectedProtocol(sslSocket)
+ : null;
+ socket = sslSocket;
+ source = Okio.buffer(Okio.source(socket));
+ sink = Okio.buffer(Okio.sink(socket));
+ handshake = unverifiedHandshake;
+ protocol = maybeProtocol != null
+ ? Protocol.get(maybeProtocol)
+ : Protocol.HTTP_1_1;
+ success = true;
+ } catch (AssertionError e) {
+ if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+ throw e;
+ } finally {
+ if (sslSocket != null) {
+ Platform.get().afterHandshake(sslSocket);
+ }
+ if (!success) {
+ closeQuietly(sslSocket);
+ }
+ }
+ }
+
+ private static SSLSocketFactory lastSslSocketFactory;
+ private static TrustRootIndex lastTrustRootIndex;
+
+ /**
+ * Returns a trust root index for {@code sslSocketFactory}. This uses a static, single-element
+ * cache to avoid redoing reflection and SSL indexing in the common case where most SSL
+ * connections use the same SSL socket factory.
+ */
+ private static synchronized TrustRootIndex trustRootIndex(SSLSocketFactory sslSocketFactory) {
+ if (sslSocketFactory != lastSslSocketFactory) {
+ X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
+ lastTrustRootIndex = Platform.get().trustRootIndex(trustManager);
+ lastSslSocketFactory = sslSocketFactory;
+ }
+ return lastTrustRootIndex;
+ }
+
+ /**
+ * 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 createTunnel(int readTimeout, int writeTimeout) throws IOException {
+ // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+ Request tunnelRequest = createTunnelRequest();
+ HttpUrl url = tunnelRequest.httpUrl();
+ String requestLine = "CONNECT " + url.host() + ":" + url.port() + " HTTP/1.1";
+ while (true) {
+ Http1xStream tunnelConnection = new Http1xStream(null, source, sink);
+ source.timeout().timeout(readTimeout, MILLISECONDS);
+ sink.timeout().timeout(writeTimeout, MILLISECONDS);
+ tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
+ tunnelConnection.finishRequest();
+ Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
+ // The response body from a CONNECT should be empty, but if it is not then we should consume
+ // it before proceeding.
+ long contentLength = OkHeaders.contentLength(response);
+ if (contentLength == -1L) {
+ contentLength = 0L;
+ }
+ Source body = tunnelConnection.newFixedLengthSource(contentLength);
+ Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
+ body.close();
+
+ switch (response.code()) {
+ case HTTP_OK:
+ // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
+ // that happens, then we will have buffered bytes that are needed by the SSLSocket!
+ // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
+ // that it will almost certainly fail because the proxy has sent unexpected data.
+ if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
+ throw new IOException("TLS tunnel buffered too many bytes!");
+ }
+ return;
+
+ case HTTP_PROXY_AUTH:
+ tunnelRequest = OkHeaders.processAuthHeader(
+ route.getAddress().getAuthenticator(), response, route.getProxy());
+ if (tunnelRequest != null) continue;
+ throw new IOException("Failed to authenticate with proxy");
+
+ default:
+ throw new IOException(
+ "Unexpected response code for CONNECT: " + response.code());
+ }
+ }
+ }
+
+ /**
+ * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
+ * no tunnel is necessary. Everything in the tunnel request is sent
+ * unencrypted to the proxy server, so tunnels include only the minimum set of
+ * headers. This avoids sending potentially sensitive data like HTTP cookies
+ * to the proxy unencrypted.
+ */
+ private Request createTunnelRequest() throws IOException {
+ return new Request.Builder()
+ .url(route.getAddress().url())
+ .header("Host", Util.hostHeader(route.getAddress().url()))
+ .header("Proxy-Connection", "Keep-Alive")
+ .header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid.
+ .build();
+ }
+
+ /** Returns true if {@link #connect} has been attempted on this connection. */
+ boolean isConnected() {
+ return protocol != null;
+ }
+
+ @Override public Route getRoute() {
+ return route;
+ }
+
+ public void cancel() {
+ // Close the raw socket so we don't end up doing synchronous I/O.
+ Util.closeQuietly(rawSocket);
+ }
+
+ @Override public Socket getSocket() {
+ return socket;
+ }
+
+ public int allocationLimit() {
+ FramedConnection framedConnection = this.framedConnection;
+ return framedConnection != null
+ ? framedConnection.maxConcurrentStreams()
+ : 1;
+ }
+
+ /** Returns true if this connection is ready to host new streams. */
+ public boolean isHealthy(boolean doExtensiveChecks) {
+ if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
+ return false;
+ }
+
+ if (framedConnection != null) {
+ return true; // TODO: check framedConnection.shutdown.
+ }
+
+ if (doExtensiveChecks) {
+ try {
+ int readTimeout = socket.getSoTimeout();
+ try {
+ socket.setSoTimeout(1);
+ if (source.exhausted()) {
+ return false; // Stream is exhausted; socket is closed.
+ }
+ return true;
+ } finally {
+ socket.setSoTimeout(readTimeout);
+ }
+ } catch (SocketTimeoutException ignored) {
+ // Read timed out; socket is good.
+ } catch (IOException e) {
+ return false; // Couldn't read; socket is closed.
+ }
+ }
+
+ return true;
+ }
+
+ @Override public Handshake getHandshake() {
+ return handshake;
+ }
+
+ /**
+ * Returns true if this is a SPDY connection. Such connections can be used
+ * in multiple HTTP requests simultaneously.
+ */
+ public boolean isMultiplexed() {
+ return framedConnection != null;
+ }
+
+ @Override public Protocol getProtocol() {
+ return protocol != null ? protocol : Protocol.HTTP_1_1;
+ }
+
+ @Override public String toString() {
+ return "Connection{"
+ + route.getAddress().url().host() + ":" + route.getAddress().url().port()
+ + ", proxy="
+ + route.getProxy()
+ + " hostAddress="
+ + route.getSocketAddress()
+ + " cipherSuite="
+ + (handshake != null ? handshake.cipherSuite() : "none")
+ + " protocol="
+ + protocol
+ + '}';
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java
new file mode 100644
index 0000000..0beba94
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A index of trusted root certificates that exploits knowledge of Android implementation details.
+ * This class is potentially much faster to initialize than {@link RealTrustRootIndex} because
+ * it doesn't need to load and index trusted CA certificates.
+ */
+public final class AndroidTrustRootIndex implements TrustRootIndex {
+ private final X509TrustManager trustManager;
+ private final Method findByIssuerAndSignatureMethod;
+
+ public AndroidTrustRootIndex(
+ X509TrustManager trustManager, Method findByIssuerAndSignatureMethod) {
+ this.findByIssuerAndSignatureMethod = findByIssuerAndSignatureMethod;
+ this.trustManager = trustManager;
+ }
+
+ @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) {
+ try {
+ TrustAnchor trustAnchor = (TrustAnchor) findByIssuerAndSignatureMethod.invoke(
+ trustManager, cert);
+ return trustAnchor != null
+ ? trustAnchor.getTrustedCert()
+ : null;
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ } catch (InvocationTargetException e) {
+ return null;
+ }
+ }
+
+ public static TrustRootIndex get(X509TrustManager trustManager) {
+ // From org.conscrypt.TrustManagerImpl, we want the method with this signature:
+ // private TrustAnchor findTrustAnchorByIssuerAndSignature(X509Certificate lastCert);
+ try {
+ Method method = trustManager.getClass().getDeclaredMethod(
+ "findTrustAnchorByIssuerAndSignature", X509Certificate.class);
+ method.setAccessible(true);
+ return new AndroidTrustRootIndex(trustManager, method);
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java
new file mode 100644
index 0000000..0e53298
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java
@@ -0,0 +1,117 @@
+/*
+ * 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.internal.tls;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+/**
+ * Computes the effective certificate chain from the raw array returned by Java's built in TLS APIs.
+ * Cleaning a chain returns a list of certificates where the first element is {@code chain[0]}, each
+ * certificate is signed by the certificate that follows, and the last certificate is a trusted CA
+ * certificate.
+ *
+ * <p>Use of the chain cleaner is necessary to omit unexpected certificates that aren't relevant to
+ * the TLS handshake and to extract the trusted CA certificate for the benefit of certificate
+ * pinning.
+ *
+ * <p>This class includes code from <a href="https://conscrypt.org/">Conscrypt's</a> {@code
+ * TrustManagerImpl} and {@code TrustedCertificateIndex}.
+ */
+public final class CertificateChainCleaner {
+ /** The maximum number of signers in a chain. We use 9 for consistency with OpenSSL. */
+ private static final int MAX_SIGNERS = 9;
+
+ private final TrustRootIndex trustRootIndex;
+
+ public CertificateChainCleaner(TrustRootIndex trustRootIndex) {
+ this.trustRootIndex = trustRootIndex;
+ }
+
+ /**
+ * Returns a cleaned chain for {@code chain}.
+ *
+ * <p>This method throws if the complete chain to a trusted CA certificate cannot be constructed.
+ * This is unexpected unless the trust root index in this class has a different trust manager than
+ * what was used to establish {@code chain}.
+ */
+ public List<Certificate> clean(List<Certificate> chain) throws SSLPeerUnverifiedException {
+ Deque<Certificate> queue = new ArrayDeque<>(chain);
+ List<Certificate> result = new ArrayList<>();
+ result.add(queue.removeFirst());
+ boolean foundTrustedCertificate = false;
+
+ followIssuerChain:
+ for (int c = 0; c < MAX_SIGNERS; c++) {
+ X509Certificate toVerify = (X509Certificate) result.get(result.size() - 1);
+
+ // If this cert has been signed by a trusted cert, use that. Add the trusted certificate to
+ // the end of the chain unless it's already present. (That would happen if the first
+ // certificate in the chain is itself a self-signed and trusted CA certificate.)
+ X509Certificate trustedCert = trustRootIndex.findByIssuerAndSignature(toVerify);
+ if (trustedCert != null) {
+ if (result.size() > 1 || !toVerify.equals(trustedCert)) {
+ result.add(trustedCert);
+ }
+ if (verifySignature(trustedCert, trustedCert)) {
+ return result; // The self-signed cert is a root CA. We're done.
+ }
+ foundTrustedCertificate = true;
+ continue;
+ }
+
+ // Search for the certificate in the chain that signed this certificate. This is typically the
+ // next element in the chain, but it could be any element.
+ for (Iterator<Certificate> i = queue.iterator(); i.hasNext(); ) {
+ X509Certificate signingCert = (X509Certificate) i.next();
+ if (verifySignature(toVerify, signingCert)) {
+ i.remove();
+ result.add(signingCert);
+ continue followIssuerChain;
+ }
+ }
+
+ // We've reached the end of the chain. If any cert in the chain is trusted, we're done.
+ if (foundTrustedCertificate) {
+ return result;
+ }
+
+ // The last link isn't trusted. Fail.
+ throw new SSLPeerUnverifiedException("Failed to find a trusted cert that signed " + toVerify);
+ }
+
+ throw new SSLPeerUnverifiedException("Certificate chain too long: " + result);
+ }
+
+ /** Returns true if {@code toVerify} was signed by {@code signingCert}'s public key. */
+ private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) {
+ if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) return false;
+ try {
+ toVerify.verify(signingCert.getPublicKey());
+ return true;
+ } catch (GeneralSecurityException verifyFailed) {
+ return false;
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java
new file mode 100644
index 0000000..885eea4
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 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.tls;
+
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.security.auth.x500.X500Principal;
+
+public final class RealTrustRootIndex implements TrustRootIndex {
+ private final Map<X500Principal, List<X509Certificate>> subjectToCaCerts;
+
+ public RealTrustRootIndex(X509Certificate... caCerts) {
+ subjectToCaCerts = new LinkedHashMap<>();
+ for (X509Certificate caCert : caCerts) {
+ X500Principal subject = caCert.getSubjectX500Principal();
+ List<X509Certificate> subjectCaCerts = subjectToCaCerts.get(subject);
+ if (subjectCaCerts == null) {
+ subjectCaCerts = new ArrayList<>(1);
+ subjectToCaCerts.put(subject, subjectCaCerts);
+ }
+ subjectCaCerts.add(caCert);
+ }
+ }
+
+ @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) {
+ X500Principal issuer = cert.getIssuerX500Principal();
+ List<X509Certificate> subjectCaCerts = subjectToCaCerts.get(issuer);
+ if (subjectCaCerts == null) return null;
+
+ for (X509Certificate caCert : subjectCaCerts) {
+ PublicKey publicKey = caCert.getPublicKey();
+ try {
+ cert.verify(publicKey);
+ return caCert;
+ } catch (Exception ignored) {
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
similarity index 65%
copy from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
copy to okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
index 4020bf4..6b0036b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,13 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.tls;
-import com.squareup.okhttp.Protocol;
+import java.security.cert.X509Certificate;
-public class HttpOverSpdy3Test extends HttpOverSpdyTest {
-
- public HttpOverSpdy3Test() {
- super(Protocol.SPDY_3);
- }
+public interface TrustRootIndex {
+ /** Returns the trusted CA certificate that signed {@code cert}. */
+ X509Certificate findByIssuerAndSignature(X509Certificate cert);
}
diff --git a/okio/README.android b/okio/README.android
index a143b63..63024bd 100644
--- a/okio/README.android
+++ b/okio/README.android
@@ -5,8 +5,11 @@
Local patches
-------------
-All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
+All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END.
+
+These changes relate to okio dependencies not available on Android, such as:
- Removal of reference to a codehause annotation used in
okio/src/main/java/okio/DeflaterSink.java
- Commenting of code that references APIs not present on Android.
- Removal of test code that uses JUnit 4.11 features such as @Parameterized.Parameters
+
diff --git a/pom.xml b/pom.xml
index 7219018..4654188 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
<packaging>pom</packaging>
<name>OkHttp (Parent)</name>
@@ -31,6 +31,8 @@
<module>okhttp-ws</module>
<module>okhttp-ws-tests</module>
+ <module>okhttp-logging-interceptor</module>
+
<module>okcurl</module>
<module>mockwebserver</module>
<module>samples</module>
@@ -52,6 +54,7 @@
<apache.http.version>4.2.2</apache.http.version>
<airlift.version>0.6</airlift.version>
<guava.version>16.0</guava.version>
+ <android.version>4.1.1.4</android.version>
<!-- Test Dependencies -->
<junit.version>4.11</junit.version>
@@ -61,7 +64,7 @@
<url>https://github.com/square/okhttp/</url>
<connection>scm:git:https://github.com/square/okhttp.git</connection>
<developerConnection>scm:git:git@github.com:square/okhttp.git</developerConnection>
- <tag>HEAD</tag>
+ <tag>parent-2.7.5</tag>
</scm>
<issueManagement>
@@ -113,6 +116,11 @@
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.google.android</groupId>
+ <artifactId>android</artifactId>
+ <version>${android.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
index aebd0eb..e92d9ed 100644
--- a/samples/crawler/pom.xml
+++ b/samples/crawler/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>crawler</artifactId>
diff --git a/samples/guide/pom.xml b/samples/guide/pom.xml
index 55e2671..a72fd64 100644
--- a/samples/guide/pom.xml
+++ b/samples/guide/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>guide</artifactId>
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
index 8e5334a..2c6cfa0 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
@@ -40,7 +40,7 @@
RequestBody requestBody = new MultipartBuilder()
.type(MultipartBuilder.FORM)
.addFormDataPart("title", "Square Logo")
- .addFormDataPart("image", null,
+ .addFormDataPart("image", "logo-square.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
index d439e99..877812e 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
@@ -2,7 +2,9 @@
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketCall;
import com.squareup.okhttp.ws.WebSocketListener;
@@ -10,11 +12,10 @@
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import okio.Buffer;
-import okio.BufferedSource;
+import okio.ByteString;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
public final class WebSocketEcho implements WebSocketListener {
private final Executor writeExecutor = Executors.newSingleThreadExecutor();
@@ -35,9 +36,9 @@
writeExecutor.execute(new Runnable() {
@Override public void run() {
try {
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello..."));
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("...World!"));
- webSocket.sendMessage(BINARY, new Buffer().writeInt(0xdeadbeef));
+ webSocket.sendMessage(RequestBody.create(TEXT, "Hello..."));
+ webSocket.sendMessage(RequestBody.create(TEXT, "...World!"));
+ webSocket.sendMessage(RequestBody.create(BINARY, ByteString.decodeHex("deadbeef")));
webSocket.close(1000, "Goodbye, World!");
} catch (IOException e) {
System.err.println("Unable to send messages: " + e.getMessage());
@@ -46,18 +47,13 @@
});
}
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- switch (type) {
- case TEXT:
- System.out.println("MESSAGE: " + payload.readUtf8());
- break;
- case BINARY:
- System.out.println("MESSAGE: " + payload.readByteString().hex());
- break;
- default:
- throw new IllegalStateException("Unknown payload type: " + type);
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ if (message.contentType() == TEXT) {
+ System.out.println("MESSAGE: " + message.string());
+ } else {
+ System.out.println("MESSAGE: " + message.source().readByteString().hex());
}
- payload.close();
+ message.close();
}
@Override public void onPong(Buffer payload) {
diff --git a/samples/pom.xml b/samples/pom.xml
index 29f1e87..77ca5f5 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<groupId>com.squareup.okhttp.sample</groupId>
diff --git a/samples/simple-client/pom.xml b/samples/simple-client/pom.xml
index 3a1aa7d..0065b7b 100644
--- a/samples/simple-client/pom.xml
+++ b/samples/simple-client/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>simple-client</artifactId>
diff --git a/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java b/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
index e616d41..d897a9a 100644
--- a/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
+++ b/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
@@ -5,6 +5,7 @@
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import java.io.Reader;
import java.util.Collections;
import java.util.Comparator;
@@ -34,8 +35,10 @@
Response response = client.newCall(request).execute();
// Deserialize HTTP response to concrete type.
- Reader body = response.body().charStream();
- List<Contributor> contributors = GSON.fromJson(body, CONTRIBUTORS.getType());
+ ResponseBody body = response.body();
+ Reader charStream = body.charStream();
+ List<Contributor> contributors = GSON.fromJson(charStream, CONTRIBUTORS.getType());
+ body.close();
// Sort list by the most contributions.
Collections.sort(contributors, new Comparator<Contributor>() {
diff --git a/samples/static-server/pom.xml b/samples/static-server/pom.xml
index f223151..75b492d 100644
--- a/samples/static-server/pom.xml
+++ b/samples/static-server/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>static-server</artifactId>
diff --git a/website/index.html b/website/index.html
index 86695a4..ecc7c69 100644
--- a/website/index.html
+++ b/website/index.html
@@ -155,7 +155,7 @@
</ul>
<ul class="nav nav-pills nav-stacked secondary">
<li><a href="https://github.com/square/okhttp/wiki">Wiki</a></li>
- <li><a href="javadoc/index.html">Javadoc</a></li>
+ <li><a href="2.x/okhttp/">Javadoc</a></li>
<li><a href="http://stackoverflow.com/questions/tagged/okhttp?sort=active">StackOverflow</a></li>
</ul>
</div>