Pull latest OkHttp code from upstream
okio:
okio is now managed upstream as a separate project but has
been included here as a sub directory: the okio version here
is intended only for use with OkHttp.
okio is synced to upstream commit
82358df7f09e18aa42348836c614212085bbf045.
See okio/README.android for local changed needed to make it
compile.
okhttp:
This is effectively an upgrade from a snapshot close to
OkHttp 1.5 with Android additions to a snapshot close to
OkHttp 2.2.
okhttp was synced to upstream commit
0a197466608681593cc9be9487965a0b1d5c244c
See README.android for local changes needed to make it
compile.
Most of the old Android changes have been pushed upstream
and other upstream changes have been made to keep OkHttp
working on Android.
TLS fallback changes have not been upstreamed yet:
bcce0a3d26d66d33beb742ae2adddb3b7db5ad08
ede2bf1af0917482da8ccb7b048130592034253d
This means that some CTS tests will start to fail. A later
commit will fix those changes when it has been accepted
upstream.
There are associated changes in libcore and frameworks/base.
Change-Id: I0a68b27b1ec7067be452671bc591edfd84e310f2
diff --git a/Android.mk b/Android.mk
index cb5adf6..7a3546f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,14 +16,19 @@
LOCAL_PATH := $(call my-dir)
okhttp_common_src_files := $(call all-java-files-under,okhttp/src/main/java)
-okhttp_common_src_files += $(call all-java-files-under,okio/src/main/java)
+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)
okhttp_test_src_files := $(call all-java-files-under,okhttp-tests/src/test/java)
+okhttp_test_src_files += $(call all-java-files-under,okhttp-urlconnection/src/test/java)
+okhttp_test_src_files += $(call all-java-files-under,okhttp-android-support/src/test/java)
+okhttp_test_src_files += $(call all-java-files-under,okio/okio/src/test/java)
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)
okhttp_test_src_files += $(call all-java-files-under,android/test/java)
-okhttp_test_src_files := $(filter-out mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java, $(okhttp_test_src_files))
include $(CLEAR_VARS)
LOCAL_MODULE := okhttp
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6140fb7..71a5041 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,315 @@
Change Log
==========
+## VERSION 2.2.0
+
+_2014-12-30_
+
+ * **`RequestBody.contentLength()` now throws `IOException`.**
+ This is a source-incompatible change. If you have code that calls
+ `RequestBody.contentLength()`, your compile will break with this
+ update. The change is binary-compatible, however: code compiled
+ for OkHttp 2.0 and 2.1 will continue work with this update.
+
+ * **`COMPATIBLE_TLS` no longer supports SSLv3.** In response to the
+ [POODLE](http://googleonlinesecurity.blogspot.ca/2014/10/this-poodle-bites-exploiting-ssl-30.html)
+ vulnerability, OkHttp no longer offers SSLv3 when negotiation an
+ HTTPS connection. If you continue to need to connect to webservers
+ running SSLv3, you must manually configure your own `ConnectionSpec`.
+
+ * **OkHttp now offers interceptors.** Interceptors are a powerful mechanism
+ that can monitor, rewrite, and retry calls. The [project
+ wiki](https://github.com/square/okhttp/wiki/Interceptors) has a full
+ introduction to this new API.
+
+ * New: APIs to iterate and selectively clear the response cache.
+ * New: Support for SOCKS proxies.
+ * New: Support for `TLS_FALLBACK_SCSV`.
+ * New: Update HTTP/2 support to to `h2-16` and `hpack-10`.
+ * New: APIs to prevent retrying non-idempotent requests.
+ * Fix: Drop NPN support. Going forward we support ALPN only.
+ * Fix: The hostname verifier is now strict. This is consistent with the hostname
+ verifier in modern browsers.
+ * Fix: Improve `CONNECT` handling for misbehaving HTTP proxies.
+ * Fix: Don't retry requests that failed due to timeouts.
+ * Fix: Cache 302s and 308s that include appropriate response headers.
+ * Fix: Improve pooling of connections that use proxy selectors.
+ * Fix: Don't leak connections when using ALPN on the desktop.
+ * Fix: Update Jetty ALPN to `7.1.2.v20141202` (Java 7) and `8.1.2.v20141202` (Java 8).
+ This fixes a bug in resumed TLS sessions where the wrong protocol could be
+ selected.
+ * Fix: Don't crash in SPDY and HTTP/2 when disconnecting before connecting.
+ * Fix: Avoid a reverse DNS-lookup for a numeric proxy address
+ * Fix: Resurrect http/2 frame logging.
+ * Fix: Limit to 20 authorization attempts.
+
+## VERSION 2.1.0
+
+_2014-11-11_
+
+ * New: Typesafe APIs for interacting with cipher suites and TLS versions.
+ * Fix: Don't crash when mixing authorization challenges with upload retries.
+
+
+## VERSION 2.1.0-RC1
+
+_2014-11-04_
+
+ * **OkHttp now caches private responses**. We've changed from a shared cache
+ to a private cache, and will now store responses that use an `Authorization`
+ header. This means OkHttp's cache shouldn't be used on middleboxes that sit
+ between user agents and the origin server.
+
+ * **TLS configuration updated.** OkHttp now explicitly enables TLSv1.2,
+ TLSv1.1 and TLSv1.0 where they are supported. It will continue to perform
+ only one fallback, to SSLv3. Applications can now configure this with the
+ `ConnectionSpec` class.
+
+ To disable TLS fallback:
+
+ ```
+ client.setConnectionSpecs(Arrays.asList(
+ ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
+ ```
+
+ To disable cleartext connections, permitting `https` URLs only:
+
+ ```
+ client.setConnectionSpecs(Arrays.asList(
+ ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS));
+ ```
+
+ * **New cipher suites.** Please confirm that your webservers are reachable
+ with this limited set of cipher suites.
+
+ ```
+ Android
+ Name Version
+
+ TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 5.0
+ TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 5.0
+ TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 5.0
+ TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA 4.0
+ TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA 4.0
+ TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 4.0
+ TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA 4.0
+ TLS_ECDHE_ECDSA_WITH_RC4_128_SHA 4.0
+ TLS_ECDHE_RSA_WITH_RC4_128_SHA 4.0
+ TLS_DHE_RSA_WITH_AES_128_CBC_SHA 2.3
+ TLS_DHE_DSS_WITH_AES_128_CBC_SHA 2.3
+ TLS_DHE_RSA_WITH_AES_256_CBC_SHA 2.3
+ TLS_RSA_WITH_AES_128_GCM_SHA256 5.0
+ TLS_RSA_WITH_AES_128_CBC_SHA 2.3
+ TLS_RSA_WITH_AES_256_CBC_SHA 2.3
+ SSL_RSA_WITH_3DES_EDE_CBC_SHA 2.3 (Deprecated in 5.0)
+ SSL_RSA_WITH_RC4_128_SHA 2.3
+ SSL_RSA_WITH_RC4_128_MD5 2.3 (Deprecated in 5.0)
+ ```
+
+ * **Okio updated to 1.0.1.**
+
+ ```
+ <dependency>
+ <groupId>com.squareup.okio</groupId>
+ <artifactId>okio</artifactId>
+ <version>1.0.1</version>
+ </dependency>
+ ```
+
+ * **New APIs to permit easy certificate pinning.** Be warned, certificate
+ pinning is dangerous and could prevent your application from trusting your
+ server!
+
+ * **Cache improvements.** This release fixes some severe cache problems
+ including a bug where the cache could be corrupted upon certain access
+ patterns. We also fixed a bug where the cache was being cleared due to a
+ corrupted journal. We've added APIs to configure a request's `Cache-Control`
+ headers, and to manually clear the cache.
+
+ * **Request cancellation fixes.** This update fixes a bug where synchronous
+ requests couldn't be canceled by tag. This update avoids crashing when
+ `onResponse()` throws an `IOException`. That failure will now be logged
+ instead of notifying the thread's uncaught exception handler. We've added a
+ new API, `Call.isCanceled()` to check if a call has been canceled.
+
+ * New: Update `MultipartBuilder` to support content length.
+ * New: Make it possible to mock `OkHttpClient` and `Call`.
+ * New: Update to h2-14 and hpack-9.
+ * New: OkHttp includes a user-agent by default, like `okhttp/2.1.0-RC1`.
+ * Fix: Handle response code `308 Permanent Redirect`.
+ * Fix: Don't skip the callback if a call is canceled.
+ * Fix: Permit hostnames with underscores.
+ * Fix: Permit overriding the content-type in `OkApacheClient`.
+ * Fix: Use the socket factory for direct connections.
+ * Fix: Honor `OkUrlFactory` APIs that disable redirects.
+ * Fix: Don't crash on concurrent modification of `SPDY` SPDY settings.
+
+## Version 2.0.0
+
+This release commits to a stable 2.0 API. Read the 2.0.0-RC1 changes for advice
+on upgrading from 1.x to 2.x.
+
+_2014-06-21_
+
+ * **API Change**: Use `IOException` in `Callback.onFailure()`. This is
+ a source-incompatible change, and is different from OkHttp 2.0.0-RC2 which
+ used `Throwable`.
+ * Fix: Fixed a caching bug where we weren't storing rewritten request headers
+ like `Accept-Encoding`.
+ * Fix: Fixed bugs in handling the SPDY window size. This was stalling certain
+ large downloads
+ * Update the language level to Java 7. (OkHttp requires Android 2.3+ or Java 7+.)
+
+## Version 2.0.0-RC2
+
+_2014-06-11_
+
+This update fixes problems in 2.0.0-RC1. Read the 2.0.0-RC1 changes for
+advice on upgrading from 1.x to 2.x.
+
+ * Fix: Don't leak connections! There was a regression in 2.0.0-RC1 where
+ connections were neither closed nor pooled.
+ * Fix: Revert builder-style return types from OkHttpClient's timeout methods
+ for binary compatibility with OkHttp 1.x.
+ * Fix: Don't skip client stream 1 on SPDY/3.1. This fixes SPDY connectivity to
+ `https://google.com`, which doesn't follow the SPDY/3.1 spec!
+ * Fix: Always configure NPN headers. This fixes connectivity to
+ `https://facebook.com` when SPDY and HTTP/2 are both disabled. Otherwise an
+ unexpected NPN response is received and OkHttp crashes.
+ * Fix: Write continuation frames when HPACK data is larger than 16383 bytes.
+ * Fix: Don't drop uncaught exceptions thrown in async calls.
+ * Fix: Throw an exception eagerly when a request body is not legal. Previously
+ we ignored the problem at request-building time, only to crash later with a
+ `NullPointerException`.
+ * Fix: Include a backwards-compatible `OkHttp-Response-Source` header with
+ `OkUrlFactory `responses.
+ * Fix: Don't include a default User-Agent header in requests made with the Call
+ API. Requests made with OkUrlFactory will continue to have a default user
+ agent.
+ * New: Guava-like API to create headers:
+
+ ```
+ Headers headers = Headers.of(name1, value1, name2, value2, ...).
+ ```
+
+ * New: Make the content-type header optional for request bodies.
+ * New: `Response.isSuccessful()` is a convenient API to check response codes.
+ * New: The response body can now be read outside of the callback. Response
+ bodies must always be closed, otherwise they will leak connections!
+ * New: APIs to create multipart request bodies (`MultipartBuilder`) and form
+ encoding bodies (`FormEncodingBuilder`).
+
+## Version 2.0.0-RC1
+
+_2014-05-23_
+
+OkHttp 2 is designed around a new API that is true to HTTP, with classes for
+requests, responses, headers, and calls. It uses modern Java patterns like
+immutability and chained builders. The API now offers asynchronous callbacks
+in addition to synchronous blocking calls.
+
+#### API Changes
+
+ * **New Request and Response types,** each with their own builder. There's also
+ a `RequestBody` class to write the request body to the network and a
+ `ResponseBody` to read the response body from the network. The standalone
+ `Headers` class offers full access to the HTTP headers.
+
+ * **Okio dependency added.** OkHttp now depends on
+ [Okio](https://github.com/square/okio), an I/O library that makes it easier
+ to access, store and process data. Using this library internally makes OkHttp
+ faster while consuming less memory. You can write a `RequestBody` as an Okio
+ `BufferedSink` and a `ResponseBody` as an Okio `BufferedSource`. Standard
+ `InputStream` and `OutputStream` access is also available.
+
+ * **New Call and Callback types** execute requests and receive their
+ responses. Both types of calls can be canceled via the `Call` or the
+ `OkHttpClient`.
+
+ * **URLConnection support has moved to the okhttp-urlconnection module.**
+ If you're upgrading from 1.x, this change will impact you. You will need to
+ add the `okhttp-urlconnection` module to your project and use the
+ `OkUrlFactory` to create new instances of `HttpURLConnection`:
+
+ ```
+ // OkHttp 1.x:
+ HttpURLConnection connection = client.open(url);
+
+ // OkHttp 2.x:
+ HttpURLConnection connection = new OkUrlFactory(client).open(url);
+ ```
+
+ * **Custom caches are no longer supported.** In OkHttp 1.x it was possible to
+ define your own response cache with the `java.net.ResponseCache` and OkHttp's
+ `OkResponseCache` interfaces. Both of these APIs have been dropped. In
+ OkHttp 2 the built-in disk cache is the only supported response cache.
+
+ * **HttpResponseCache has been renamed to Cache.** Install it with
+ `OkHttpClient.setCache(...)` instead of `OkHttpClient.setResponseCache(...)`.
+
+ * **OkAuthenticator has been replaced with Authenticator.** This new
+ authenticator has access to the full incoming response and can respond with
+ whichever followup request is appropriate. The `Challenge` class is now a
+ top-level class and `Credential` is replaced with a utility class called
+ `Credentials`.
+
+ * **OkHttpClient.getFollowProtocolRedirects() renamed to
+ getFollowSslRedirects()**. We reserve the word _protocol_ for the HTTP
+ version being used (HTTP/1.1, HTTP/2). The old name of this method was
+ misleading; it was always used to configure redirects between `https://` and
+ `http://` schemes.
+
+ * **RouteDatabase is no longer public API.** OkHttp continues to track which
+ routes have failed but this is no exposed in the API.
+
+ * **ResponseSource is gone.** This enum exposed whether a response came from
+ the cache, network, or both. OkHttp 2 offers more detail with raw access to
+ the cache and network responses in the new `Response` class.
+
+ * **TunnelRequest is gone.** It specified how to connect to an HTTP proxy.
+ OkHttp 2 uses the new `Request` class for this.
+
+ * **Dispatcher** is a new class to manages the queue of asynchronous calls. It
+ implements limits on total in-flight calls and in-flight calls per host.
+
+#### Implementation changes
+
+ * Support Android `TrafficStats` socket tagging.
+ * Drop authentication headers on redirect.
+ * Added support for compressed data frames.
+ * Process push promise callbacks in order.
+ * Update to http/2 draft 12.
+ * Update to HPACK draft 07.
+ * Add ALPN support. Maven will use ALPN on OpenJDK 8.
+ * Update NPN dependency to target `jdk7u60-b13` and `Oracle jdk7u55-b13`.
+ * Ensure SPDY variants support zero-length DELETE and POST.
+ * Prevent leaking a cache item's InputStreams when metadata read fails.
+ * Use a string to identify TLS versions in routes.
+ * Add frame logger for HTTP/2.
+ * Replacing `httpMinorVersion` with `Protocol`. Expose HTTP/1.0 as a potential protocol.
+ * Use `Protocol` to describe framing.
+ * Implement write timeouts for HTTP/1.1 streams.
+ * Avoid use of SPDY stream ID 1, as that's typically used for UPGRADE.
+ * Support OAuth in `Authenticator`.
+ * Permit a dangling semicolon in media type parsing.
+
+## Version 1.6.0
+
+_2014-05-23_
+
+ * Offer bridges to make it easier to migrate from OkHttp 1.x to OkHttp 2.0.
+ This adds `OkUrlFactory`, `Cache`, and `@Deprecated` annotations for APIs
+ dropped in 2.0.
+
+## Version 1.5.4
+
+_2014-04-14_
+
+ * Drop ALPN support in Android. There's a concurrency bug in all
+ currently-shipping versions.
+ * Support asynchronous disconnects by breaking the socket only. This should
+ prevent flakiness from multiple threads concurrently accessing a stream.
+
## Version 1.5.3
_2014-03-29_
diff --git a/README.android b/README.android
index 05c6114..0a1b91b 100644
--- a/README.android
+++ b/README.android
@@ -5,9 +5,18 @@
Local patches
-------------
-- Addition of classes in android/ :
+Addition of classes in android/ :
- com.squareup.okhttp.internal.Platform - to replace the Platform class that
comes with okhttp. No use of reflection.
- com.squareup.okhttp.Http(s)Handler - integration with Android's corelibs.
- - Removal of reference to a codehause annotation used in
- okio/src/main/java/okio/DeflaterSink.java
+ - com.squareup.okhttp.ConfigAwareConnectionPool - support for a
+ ConnectionPool that listens for network configuration changes.
+ - com.squareup.okhttp.internal.Version - a hard-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.
+
+okio/ contains a snapshot of the Okio project. See okio/README.android for
+details.
diff --git a/README.md b/README.md
index cd3bd02..0e23958 100644
--- a/README.md
+++ b/README.md
@@ -1,64 +1,26 @@
OkHttp
======
-An HTTP & SPDY client for Android and Java applications.
-
-For more information please see [the website][1].
-
-
+An HTTP & SPDY client for Android and Java applications. For more information see [the website][1] and [the wiki][2].
Download
--------
-Download [the latest JAR][2] or grab via Maven:
-
+Download [the latest JAR][3] or grab via Maven:
```xml
<dependency>
- <groupId>com.squareup.okhttp</groupId>
- <artifactId>okhttp</artifactId>
- <version>(insert latest version)</version>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>2.2.0</version>
</dependency>
```
-
-
-Building
---------
-
-OkHttp requires Java 7 to build and run tests. Runtime compatibility with Java 6 is enforced as
-part of the build to ensure compliance with Android and older versions of the JVM.
-
-
-
-Testing
--------
-
-### On the Desktop
-
-Run OkHttp tests on the desktop with Maven. Running SPDY tests on the desktop uses
-[Jetty-NPN][3] which requires OpenJDK 7+.
-
-```
-mvn clean test
+or Gradle:
+```groovy
+compile 'com.squareup.okhttp:okhttp:2.2.0'
```
-### On a Device
+Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
-OkHttp's test suite creates an in-process HTTPS server. Prior to Android 2.3, SSL server sockets
-were broken, and so HTTPS tests will time out when run on such devices.
-
-Test on a USB-attached Android using [Vogar][4]. Unfortunately `dx` requires that you build with
-Java 6, otherwise the test class will be silently omitted from the `.dex` file.
-
-```
-mvn clean
-mvn package -DskipTests
-vogar \
- --classpath ~/.m2/repository/org/bouncycastle/bcprov-jdk15on/1.48/bcprov-jdk15on-1.48.jar \
- --classpath mockwebserver/target/mockwebserver-2.0.0-SNAPSHOT.jar \
- --classpath okhttp-protocols/target/okhttp-protocols-2.0.0-SNAPSHOT.jar \
- --classpath okhttp/target/okhttp-2.0.0-SNAPSHOT.jar \
- okhttp/src/test
-```
MockWebServer
-------------
@@ -69,16 +31,20 @@
### Download
-Download [the latest JAR][5] or grab via Maven:
-
+Download [the latest JAR][4] or grab via Maven:
```xml
<dependency>
- <groupId>com.squareup.okhttp</groupId>
- <artifactId>mockwebserver</artifactId>
- <version>(insert latest version)</version>
- <scope>test</scope>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>2.2.0</version>
+ <scope>test</scope>
</dependency>
```
+or Gradle:
+```groovy
+testCompile 'com.squareup.okhttp:mockwebserver:2.2.0'
+```
+
License
@@ -97,10 +63,8 @@
limitations under the License.
-
-
[1]: http://square.github.io/okhttp
- [2]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST
- [3]: http://wiki.eclipse.org/Jetty/Feature/NPN
- [4]: https://code.google.com/p/vogar/
- [5]: http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=mockwebserver&v=LATEST
+ [2]: https://github.com/square/okhttp/wiki
+ [3]: https://search.maven.org/remote_content?g=com.squareup.okhttp&a=okhttp&v=LATEST
+ [4]: https://search.maven.org/remote_content?g=com.squareup.okhttp&a=mockwebserver&v=LATEST
+ [snap]: https://oss.sonatype.org/content/repositories/snapshots/
diff --git a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
index e64eec4..36c3101 100644
--- a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
+++ b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
@@ -86,11 +86,7 @@
// If the network config has changed then existing pooled connections should not be
// re-used. By setting connectionPool to null it ensures that the next time
// getConnectionPool() is called a new pool will be created.
- ConnectionPool oldConnectionPool = connectionPool;
connectionPool = null;
- if (oldConnectionPool != null) {
- oldConnectionPool.enterDrainMode();
- }
}
}
});
diff --git a/android/main/java/com/squareup/okhttp/HttpHandler.java b/android/main/java/com/squareup/okhttp/HttpHandler.java
index bcb47b0..a94fe8f 100644
--- a/android/main/java/com/squareup/okhttp/HttpHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpHandler.java
@@ -30,44 +30,44 @@
ConfigAwareConnectionPool.getInstance();
@Override protected URLConnection openConnection(URL url) throws IOException {
- return newOkHttpClient(null /* proxy */).open(url);
+ return newOkUrlFactory(null /* proxy */).open(url);
}
@Override protected URLConnection openConnection(URL url, Proxy proxy) throws IOException {
if (url == null || proxy == null) {
throw new IllegalArgumentException("url == null || proxy == null");
}
- return newOkHttpClient(proxy).open(url);
+ return newOkUrlFactory(proxy).open(url);
}
@Override protected int getDefaultPort() {
return 80;
}
- protected OkHttpClient newOkHttpClient(Proxy proxy) {
- OkHttpClient okHttpClient = createHttpOkHttpClient(proxy);
- okHttpClient.setConnectionPool(configAwareConnectionPool.get());
- return okHttpClient;
+ protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
+ OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);
+ okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
+ return okUrlFactory;
}
/**
* Creates an OkHttpClient suitable for creating {@link java.net.HttpURLConnection} instances on
* Android.
*/
- public static OkHttpClient createHttpOkHttpClient(Proxy proxy) {
+ public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
OkHttpClient client = new OkHttpClient();
- client.setFollowProtocolRedirects(false);
+ client.setFollowSslRedirects(false);
if (proxy != null) {
client.setProxy(proxy);
}
// Explicitly set the response cache.
+ OkUrlFactory okUrlFactory = new OkUrlFactory(client);
ResponseCache responseCache = ResponseCache.getDefault();
if (responseCache != null) {
- client.setResponseCache(responseCache);
+ AndroidInternal.setResponseCache(okUrlFactory, responseCache);
}
-
- return client;
+ return okUrlFactory;
}
}
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
index f7584fc..cfd7aba 100644
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ b/android/main/java/com/squareup/okhttp/HttpsHandler.java
@@ -24,7 +24,7 @@
import javax.net.ssl.HttpsURLConnection;
public final class HttpsHandler extends HttpHandler {
- private static final List<Protocol> ENABLED_PROTOCOLS = Arrays.asList(Protocol.HTTP_11);
+ private static final List<Protocol> ENABLED_PROTOCOLS = Arrays.asList(Protocol.HTTP_1_1);
private final ConfigAwareConnectionPool configAwareConnectionPool =
ConfigAwareConnectionPool.getInstance();
@@ -33,30 +33,31 @@
}
@Override
- protected OkHttpClient newOkHttpClient(Proxy proxy) {
- OkHttpClient okHttpClient = createHttpsOkHttpClient(proxy);
- okHttpClient.setConnectionPool(configAwareConnectionPool.get());
- return okHttpClient;
+ protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
+ OkUrlFactory okUrlFactory = createHttpsOkUrlFactory(proxy);
+ okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
+ return okUrlFactory;
}
/**
* Creates an OkHttpClient suitable for creating {@link HttpsURLConnection} instances on
* Android.
*/
- public static OkHttpClient createHttpsOkHttpClient(Proxy proxy) {
+ public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
// The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
- OkHttpClient client = HttpHandler.createHttpOkHttpClient(proxy);
+ OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
- client.setProtocols(ENABLED_PROTOCOLS);
+ OkHttpClient okHttpClient = okUrlFactory.client();
+ okHttpClient.setProtocols(ENABLED_PROTOCOLS);
// OkHttp does not automatically honor the system-wide HostnameVerifier set with
// HttpsURLConnection.setDefaultHostnameVerifier().
- client.setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
+ okUrlFactory.client().setHostnameVerifier(HttpsURLConnection.getDefaultHostnameVerifier());
// OkHttp does not automatically honor the system-wide SSLSocketFactory set with
// HttpsURLConnection.setDefaultSSLSocketFactory().
// See https://github.com/square/okhttp/issues/184 for details.
- client.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
+ okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
- return client;
+ return okUrlFactory;
}
}
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 87efb7d..328bf3a 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -26,13 +26,11 @@
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
-import java.util.zip.Deflater;
-import java.util.zip.DeflaterOutputStream;
import javax.net.ssl.SSLSocket;
import com.squareup.okhttp.Protocol;
-import okio.ByteString;
+import okio.Buffer;
/**
* Access to proprietary Android APIs. Doesn't use reflection.
@@ -73,77 +71,47 @@
return url.toURILenient();
}
- public void configureSecureSocket(SSLSocket socket, String uriHost, boolean isFallback) {
- SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(socket, true);
- SET_HOSTNAME.invokeOptionalWithoutCheckedException(socket, uriHost);
+ public void configureTlsExtensions(
+ SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
+ // Enable SNI and session tickets.
+ if (hostname != null) {
+ SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
+ SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
+ }
- 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 = false;
- String[] supportedCipherSuites = socket.getSupportedCipherSuites();
- for (int i = supportedCipherSuites.length - 1; i >= 0; i--) {
- String supportedCipherSuite = supportedCipherSuites[i];
- if (fallbackScsv.equals(supportedCipherSuite)) {
- socketSupportsFallbackScsv = true;
- break;
- }
- }
- if (socketSupportsFallbackScsv) {
- // Add the SCSV cipher to the set of enabled ciphers.
- String[] enabledCipherSuites = socket.getEnabledCipherSuites();
- String[] newEnabledCipherSuites = new String[enabledCipherSuites.length + 1];
- System.arraycopy(enabledCipherSuites, 0,
- newEnabledCipherSuites, 0, enabledCipherSuites.length);
- newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
- socket.setEnabledCipherSuites(newEnabledCipherSuites);
- }
+ // Enable NPN / ALPN.
+ boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(sslSocket);
+ if (!alpnSupported) {
+ return;
+ }
+
+ Object[] parameters = { concatLengthPrefixed(protocols) };
+ if (alpnSupported) {
+ SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
}
}
/**
- * Returns the negotiated protocol, or null if no protocol was negotiated.
+ * Called after the TLS handshake to release resources allocated by {@link
+ * #configureTlsExtensions}.
*/
- public ByteString getNpnSelectedProtocol(SSLSocket socket) {
+ public void afterHandshake(SSLSocket sslSocket) {
+ }
+
+ public String getSelectedProtocol(SSLSocket socket) {
boolean alpnSupported = GET_ALPN_SELECTED_PROTOCOL.isSupported(socket);
if (!alpnSupported) {
return null;
}
byte[] alpnResult =
- (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+ (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
if (alpnResult != null) {
- return ByteString.of(alpnResult);
+ return new String(alpnResult, Util.UTF_8);
}
return null;
}
- /**
- * Sets client-supported protocols on a socket to send to a server. The
- * protocols are only sent if the socket implementation supports NPN.
- */
- public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
- boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(socket);
- if (!alpnSupported) {
- return;
- }
-
- byte[] protocols = concatLengthPrefixed(npnProtocols);
- SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(
- socket, new Object[] { protocols });
- }
-
- /**
- * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
- * value blocks. This throws an {@link UnsupportedOperationException} on
- * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
- */
- public OutputStream newDeflaterOutputStream(
- OutputStream out, Deflater deflater, boolean syncFlush) {
- return new DeflaterOutputStream(out, deflater, syncFlush);
- }
-
public void connectSocket(Socket socket, InetSocketAddress address,
int connectTimeout) throws IOException {
socket.connect(address, connectTimeout);
@@ -155,24 +123,17 @@
}
/**
- * Concatenation of 8-bit, length prefixed protocol names.
- *
+ * Returns the concatenation of 8-bit, length prefixed protocol names.
* http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
*/
static byte[] concatLengthPrefixed(List<Protocol> protocols) {
- int size = 0;
- for (Protocol protocol : protocols) {
- size += protocol.name.size() + 1; // add a byte for 8-bit length prefix.
+ Buffer result = new Buffer();
+ for (int i = 0, size = protocols.size(); i < size; i++) {
+ Protocol protocol = protocols.get(i);
+ if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for NPN.
+ result.writeByte(protocol.toString().length());
+ result.writeUtf8(protocol.toString());
}
- byte[] result = new byte[size];
- int pos = 0;
- for (Protocol protocol : protocols) {
- int nameSize = protocol.name.size();
- result[pos++] = (byte) nameSize;
- // toByteArray allocates an array, but this is only called on new connections.
- System.arraycopy(protocol.name.toByteArray(), 0, result, pos, nameSize);
- pos += nameSize;
- }
- return result;
+ return result.readByteArray();
}
}
diff --git a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java b/android/main/java/com/squareup/okhttp/internal/Version.java
similarity index 68%
copy from okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
copy to android/main/java/com/squareup/okhttp/internal/Version.java
index ac3de72..6a63f9b 100644
--- a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
+++ b/android/main/java/com/squareup/okhttp/internal/Version.java
@@ -13,10 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package okio;
+package com.squareup.okhttp.internal;
-public final class OkBufferReadUtf8LineTest extends ReadUtf8LineTest {
- @Override protected BufferedSource newSource(String s) {
- return new OkBuffer().writeUtf8(s);
+public final class Version {
+ public static String userAgent() {
+ String agent = System.getProperty("http.agent");
+ return agent != null ? agent : ("Java" + System.getProperty("java.version"));
+ }
+
+ private Version() {
}
}
diff --git a/android/test/java/com.squareup.okhttp/ConfigAwareConnectionPoolTest.java b/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
similarity index 98%
rename from android/test/java/com.squareup.okhttp/ConfigAwareConnectionPoolTest.java
rename to android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
index 6477e10..825f980 100644
--- a/android/test/java/com.squareup.okhttp/ConfigAwareConnectionPoolTest.java
+++ b/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
@@ -17,7 +17,6 @@
package com.squareup.okhttp;
-import org.junit.Before;
import org.junit.Test;
import libcore.net.event.NetworkEventDispatcher;
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
index 60a545f..2e5dcdd 100644
--- a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -23,16 +23,18 @@
import org.junit.Test;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
+
import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import okio.ByteString;
+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;
@@ -47,39 +49,32 @@
// Expect no error
TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
- platform.configureSecureSocket(arbitrarySocketImpl, "host", false);
+ List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1, Protocol.SPDY_3);
+ platform.configureTlsExtensions(arbitrarySocketImpl, "host", protocols);
+ NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+ platform.configureTlsExtensions(npnOnlySSLSocketImpl, "host", protocols);
FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
- platform.configureSecureSocket(openSslSocket, "host", false);
+ platform.configureTlsExtensions(openSslSocket, "host", protocols);
assertTrue(openSslSocket.useSessionTickets);
assertEquals("host", openSslSocket.hostname);
+ assertArrayEquals(Platform.concatLengthPrefixed(protocols), openSslSocket.alpnProtocols);
}
@Test
- public void getNpnSelectedProtocol() throws Exception {
+ public void getSelectedProtocol() throws Exception {
Platform platform = new Platform();
- byte[] alpnBytes = "alpn".getBytes();
+ String selectedProtocol = "alpn";
TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
- assertNull(platform.getNpnSelectedProtocol(arbitrarySocketImpl));
+ assertNull(platform.getSelectedProtocol(arbitrarySocketImpl));
+
+ NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
+ assertNull(platform.getSelectedProtocol(npnOnlySSLSocketImpl));
FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
- openSslSocket.alpnProtocols = alpnBytes;
- assertEquals(ByteString.of(alpnBytes), platform.getNpnSelectedProtocol(openSslSocket));
- }
-
- @Test
- public void setNpnProtocols() throws Exception {
- Platform platform = new Platform();
- List<Protocol> protocols = Arrays.asList(Protocol.SPDY_3);
-
- // No error
- TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
- platform.setNpnProtocols(arbitrarySocketImpl, protocols);
-
- FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
- platform.setNpnProtocols(openSslSocket, protocols);
- assertNotNull(openSslSocket.alpnProtocols);
+ openSslSocket.alpnProtocols = selectedProtocol.getBytes(StandardCharsets.UTF_8);
+ assertEquals(selectedProtocol, platform.getSelectedProtocol(openSslSocket));
}
private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
@@ -112,6 +107,20 @@
}
}
+ // Legacy case - NPN support has been dropped.
+ private static class NpnOnlySSLSocketImpl extends TestSSLSocketImpl {
+
+ private byte[] npnProtocols;
+
+ public void setNpnProtocols(byte[] npnProtocols) {
+ this.npnProtocols = npnProtocols;
+ }
+
+ public byte[] getNpnSelectedProtocol() {
+ return npnProtocols;
+ }
+ }
+
private static class TestSSLSocketImpl extends SSLSocket {
@Override
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index 6425e64..4fde956 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>benchmarks</artifactId>
@@ -31,6 +31,11 @@
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-urlconnection</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
<artifactId>mockwebserver</artifactId>
<version>${project.version}</version>
</dependency>
@@ -39,11 +44,6 @@
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
- <groupId>org.mortbay.jetty.npn</groupId>
- <artifactId>npn-boot</artifactId>
- <scope>provided</scope>
- </dependency>
- <dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
@@ -71,28 +71,72 @@
</dependencies>
<build>
<plugins>
- <plugin>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>exec-maven-plugin</artifactId>
- <executions>
- <execution>
- <goals>
- <goal>java</goal>
- </goals>
- </execution>
- </executions>
- <configuration>
- <executable>java</executable>
- <arguments>
- <argument>-Xms512m</argument>
- <argument>-Xmx512m</argument>
- <commandlineArgs>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</commandlineArgs>
- <argument>-classpath</argument>
- <classpath/>
- <argument>com.squareup.okhttp.benchmarks.Benchmark</argument>
- </arguments>
- </configuration>
- </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>java</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <executable>java</executable>
+ <arguments>
+ <argument>-Xms512m</argument>
+ <argument>-Xmx512m</argument>
+ <commandlineArgs>-Xbootclasspath/p:${bootclasspath}</commandlineArgs>
+ <argument>-classpath</argument>
+ <classpath />
+ <argument>com.squareup.okhttp.benchmarks.Benchmark</argument>
+ </arguments>
+ </configuration>
+ </plugin>
</plugins>
</build>
+ <profiles>
+ <profile>
+ <id>alpn-when-jdk7</id>
+ <activation>
+ <jdk>1.7</jdk>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.mortbay.jetty.alpn</groupId>
+ <artifactId>alpn-boot</artifactId>
+ <version>${alpn.jdk7.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ </profile>
+ <profile>
+ <id>alpn-when-jdk8</id>
+ <activation>
+ <jdk>1.8</jdk>
+ </activation>
+ <dependencies>
+ <dependency>
+ <groupId>org.mortbay.jetty.alpn</groupId>
+ <artifactId>alpn-boot</artifactId>
+ <version>${alpn.jdk8.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <!-- Fails on caliper's ASM on OpenJDK 8. -->
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>none</phase>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
index 151128d..7f0073c 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/Benchmark.java
@@ -24,9 +24,7 @@
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
-import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
@@ -35,8 +33,9 @@
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.util.zip.GZIPOutputStream;
import javax.net.ssl.SSLContext;
+import okio.Buffer;
+import okio.GzipSink;
/**
* This benchmark is fake, but may be useful for certain relative comparisons.
@@ -82,11 +81,11 @@
@Param({ "0", "20" })
int headerCount;
- /** Which ALPN/NPN protocols are in use. Only useful with TLS. */
- List<Protocol> protocols = Arrays.asList(Protocol.HTTP_11);
+ /** Which ALPN protocols are in use. Only useful with TLS. */
+ List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1);
public static void main(String[] args) {
- List<String> allArgs = new ArrayList<String>();
+ List<String> allArgs = new ArrayList<>();
allArgs.add("--instrument");
allArgs.add("arbitrary");
allArgs.addAll(Arrays.asList(args));
@@ -140,7 +139,7 @@
}
@Override public String toString() {
- List<Object> modifiers = new ArrayList<Object>();
+ List<Object> modifiers = new ArrayList<>();
if (tls) modifiers.add("tls");
if (gzip) modifiers.add("gzip");
if (chunked) modifiers.add("chunked");
@@ -164,8 +163,7 @@
if (tls) {
SSLContext sslContext = SslContextBuilder.localhost();
server.useHttps(sslContext.getSocketFactory(), false);
- server.setNpnEnabled(true);
- server.setNpnProtocols(protocols);
+ server.setProtocols(protocols);
}
final MockResponse response = newResponse();
@@ -175,18 +173,23 @@
}
});
- server.play();
+ server.start();
return server;
}
private MockResponse newResponse() throws IOException {
- byte[] body = new byte[bodyByteCount];
- random.nextBytes(body);
+ byte[] bytes = new byte[bodyByteCount];
+ random.nextBytes(bytes);
+ Buffer body = new Buffer().write(bytes);
MockResponse result = new MockResponse();
if (gzip) {
- body = gzip(body);
+ Buffer gzipBody = new Buffer();
+ GzipSink gzipSink = new GzipSink(gzipBody);
+ gzipSink.write(body, body.size());
+ gzipSink.close();
+ body = gzipBody;
result.addHeader("Content-Encoding: gzip");
}
@@ -211,13 +214,4 @@
}
return new String(result);
}
-
- /** Returns a gzipped copy of {@code bytes}. */
- private byte[] gzip(byte[] bytes) throws IOException {
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
- gzippedOut.write(bytes);
- gzippedOut.close();
- return bytesOut.toByteArray();
- }
}
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
index 9044d0a..5d8cec5 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/NettyHttpClient.java
@@ -53,8 +53,8 @@
private static final boolean VERBOSE = false;
// Guarded by this. Real apps need more capable connection management.
- private final Deque<HttpChannel> freeChannels = new ArrayDeque<HttpChannel>();
- private final Deque<URL> backlog = new ArrayDeque<URL>();
+ private final Deque<HttpChannel> freeChannels = new ArrayDeque<>();
+ private final Deque<URL> backlog = new ArrayDeque<>();
private int totalChannels = 0;
private int concurrencyLevel;
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
index 03b9e3c..3885ed7 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttp.java
@@ -16,6 +16,7 @@
package com.squareup.okhttp.benchmarks;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.IOException;
import java.net.HttpURLConnection;
@@ -63,7 +64,7 @@
public void run() {
long start = System.nanoTime();
try {
- HttpURLConnection urlConnection = client.open(url);
+ HttpURLConnection urlConnection = new OkUrlFactory(client).open(url);
long total = readAllAndClose(urlConnection.getInputStream());
long finish = System.nanoTime();
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
index b7633b7..ab78490 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/OkHttpAsync.java
@@ -15,11 +15,12 @@
*/
package com.squareup.okhttp.benchmarks;
+import com.squareup.okhttp.Callback;
import com.squareup.okhttp.Dispatcher;
-import com.squareup.okhttp.Failure;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.SslContextBuilder;
import java.io.IOException;
import java.net.URL;
@@ -38,7 +39,7 @@
private final AtomicInteger requestsInFlight = new AtomicInteger();
private OkHttpClient client;
- private Response.Receiver receiver;
+ private Callback callback;
private int concurrencyLevel;
private int targetBacklog;
@@ -63,13 +64,13 @@
client.setHostnameVerifier(hostnameVerifier);
}
- receiver = new Response.Receiver() {
- @Override public void onFailure(Failure failure) {
- System.out.println("Failed: " + failure.exception());
+ callback = new Callback() {
+ @Override public void onFailure(Request request, IOException e) {
+ System.out.println("Failed: " + e);
}
- @Override public boolean onResponse(Response response) throws IOException {
- Response.Body body = response.body();
+ @Override public void onResponse(Response response) throws IOException {
+ ResponseBody body = response.body();
long total = SynchronousHttpClient.readAllAndClose(body.byteStream());
long finish = System.nanoTime();
if (VERBOSE) {
@@ -78,14 +79,13 @@
total, TimeUnit.NANOSECONDS.toMillis(finish - start));
}
requestsInFlight.decrementAndGet();
- return true;
}
};
}
@Override public void enqueue(URL url) throws Exception {
requestsInFlight.incrementAndGet();
- client.enqueue(new Request.Builder().tag(System.nanoTime()).url(url).build(), receiver);
+ client.newCall(new Request.Builder().tag(System.nanoTime()).url(url).build()).enqueue(callback);
}
@Override public synchronized boolean acceptingJobs() {
diff --git a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
index 79abb69..630ec91 100644
--- a/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
+++ b/benchmarks/src/main/java/com/squareup/okhttp/benchmarks/UrlConnection.java
@@ -16,7 +16,6 @@
package com.squareup.okhttp.benchmarks;
import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
@@ -24,6 +23,7 @@
import java.util.concurrent.TimeUnit;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
@@ -41,8 +41,8 @@
return true;
}
};
- HttpsURLConnectionImpl.setDefaultHostnameVerifier(hostnameVerifier);
- HttpsURLConnectionImpl.setDefaultSSLSocketFactory(socketFactory);
+ HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier);
+ HttpsURLConnection.setDefaultSSLSocketFactory(socketFactory);
}
}
diff --git a/checkstyle.xml b/checkstyle.xml
index 794af42..f725be3 100644
--- a/checkstyle.xml
+++ b/checkstyle.xml
@@ -56,7 +56,9 @@
<module name="IllegalImport"/>
<!-- defaults to sun.* packages -->
<module name="RedundantImport"/>
- <module name="UnusedImports"/>
+ <module name="UnusedImports">
+ <property name="processJavadoc" value="true"/>
+ </module>
<!-- Checks for Size Violations. -->
diff --git a/concurrency.md b/concurrency.md
deleted file mode 100644
index 0858133..0000000
--- a/concurrency.md
+++ /dev/null
@@ -1,63 +0,0 @@
-# Concurrency in OkHttp
-
-The HttpURLConnection API is a blocking API. You make a blocking write to send a request, and a blocking read to receive the response.
-
-#### Blocking APIs
-
-Blocking APIs are convenient because you get top-to-bottom procedural code without indirection. Network calls work like regular method calls: ask for data and it is returned. If the request fails, you get a stacktrace right were the call was made.
-
-Blocking APIs may be inefficient because you hold a thread idle while waiting on the network. Threads are expensive because they have both a memory overhead and a context-switching overhead.
-
-#### Framed protocols
-
-Framed protocols like spdy/3 and http/2 don't lend themselves to blocking APIs. Each application-layer thread wants to do blocking I/O for a specific stream, but the streams are multiplexed on the socket. You can't just talk to the socket, you need to cooperate with the other application-layer threads that you're sharing it with.
-
-Framing rules make it impractical to implement spdy/3 or http/2 correctly on a single blocking thread. The flow-control features introduce feedback between reads and writes, requiring writes to acknowledge reads and reads to throttle writes.
-
-In OkHttp we expose a blocking API over a framed protocol. This document explains the code and policy that makes that work.
-
-## Threads
-
-#### Application's calling thread
-
-The application-layer must block on writing I/O. We can't return from a write until we've pushed its bytes onto the socket. Otherwise, if the write fails we are unable to deliver its IOException to the application. We would have told the application layer that the write succeeded, but it didn't!
-
-The application-layer can also do blocking reads. If the application asks to read and there's nothing available, we need to hold that thread until either the bytes arrive, the stream is closed, or a timeout elapses. If we get bytes but there's nobody asking for them, we buffer them. We don't consider bytes as delivered for flow control until they're consumed by the application.
-
-Consider an application streaming a video over http/2. Perhaps the user pauses the video and the application stops reading bytes from this stream. The buffer will fill up, and flow control prevents the server from sending more data on this stream. When the user unpauses her video the buffer drains, the read is acknowledged, and the server proceeds to stream data.
-
-#### Shared reader thread
-
-We can't rely on application threads to read data from the socket. Application threads are transient: sometimes they're reading and writing and sometimes they're off doing application-layer things. But the socket is permanent, and it needs constant attention: we dispatch all incoming frames so the connection is good-to-go when the application layer needs it.
-
-So we have a dedicated thread for every socket that just reads frames and dispatches them.
-
-The reader thread must never run application-layer code. Otherwise one slow stream can hold up the entire connection.
-
-Similarly, the reader thread must never block on writing because this can deadlock the connection. Consider a client and server that both violate this rule. If you get unlucky, they could fill up their TCP buffers (so that writes block) and then use their reader threads to write a frame. Nobody is reading on either end, and the buffers are never drained.
-
-#### Do-stuff-later pool
-
-Sometimes there's an action required like calling the application layer or responding to a ping, and the thread discovering the action is not the thread that should do the work. We enqueue a runnable on this executor and it gets handled by one of the executor's threads.
-
-## Locks
-
-We have 3 different things that we synchronize on.
-
-#### SpdyConnection
-
-This lock guards internal state of each connection. This lock is never held for blocking operations. That means that we acquire the lock, read or write a few fields and release the lock. No I/O and no application-layer callbacks.
-
-#### SpdyStream
-
-This lock guards the internal state of each stream. As above, it is never held for blocking operations. When we need to hold an application thread to block a read, we use wait/notify on this lock. This works because the lock is released while `wait()` is waiting.
-
-#### FrameWriter
-
-Socket writes are guarded by the FrameWriter. Only one stream can write at a time so that messages are not interleaved. Writes are either made by application-layer threads or the do-stuff-later pool.
-
-### Holding multiple locks
-
-You're allowed to take the SpdyConnection lock while holding the FrameWriter lock. But not vice-versa. Because taking the FrameWriter lock can block.
-
-This is necessary for bookkeeping when creating new streams. Correct framing requires that stream IDs are sequential on the socket, so we need to bundle assigning the ID with sending the `SYN_STREAM` frame.
diff --git a/deploy_website.sh b/deploy_website.sh
index bac2744..bbeedc2 100755
--- a/deploy_website.sh
+++ b/deploy_website.sh
@@ -26,10 +26,20 @@
# Copy website files from real repo
cp -R ../website/* .
-# Download the latest javadoc
-curl -L "http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=$GROUP_ID&a=$ARTIFACT_ID&v=LATEST&c=javadoc" > javadoc.zip
-mkdir javadoc
-unzip javadoc.zip -d javadoc
+# 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
diff --git a/hostnameverifier.gradle b/hostnameverifier.gradle
deleted file mode 100644
index 0be0b50..0000000
--- a/hostnameverifier.gradle
+++ /dev/null
@@ -1,16 +0,0 @@
-apply plugin: 'java'
-
-// static library containing only the okhttp HostnameVerifier
-sourceSets {
- main {
- java {
- srcDirs = [
- 'okhttp/src/main/java/com/squareup/okhttp/internal/tls/',
- ]
- }
- }
-}
-
-compileJava {
- options.compilerArgs += ['-encoding', 'UTF-8']
-}
diff --git a/jarjar-rules.txt b/jarjar-rules.txt
index ccb5fdd..c84813d 100644
--- a/jarjar-rules.txt
+++ b/jarjar-rules.txt
@@ -1,2 +1,2 @@
rule com.squareup.** com.android.@1
-rule okio.** com.android.okio.@1
+rule okio.** com.android.okhttp.okio.@1
diff --git a/mockwebserver/README.md b/mockwebserver/README.md
index 39257be..c729668 100644
--- a/mockwebserver/README.md
+++ b/mockwebserver/README.md
@@ -27,51 +27,51 @@
Here's a complete example:
-```
- public void test() throws Exception {
- // Create a MockWebServer. These are lean enough that you can create a new
- // instance for every unit test.
- MockWebServer server = new MockWebServer();
+```java
+public void test() throws Exception {
+ // Create a MockWebServer. These are lean enough that you can create a new
+ // instance for every unit test.
+ MockWebServer server = new MockWebServer();
- // Schedule some responses.
- server.enqueue(new MockResponse().setBody("hello, world!"));
- server.enqueue(new MockResponse().setBody("sup, bra?"));
- server.enqueue(new MockResponse().setBody("yo dog"));
+ // Schedule some responses.
+ server.enqueue(new MockResponse().setBody("hello, world!"));
+ server.enqueue(new MockResponse().setBody("sup, bra?"));
+ server.enqueue(new MockResponse().setBody("yo dog"));
- // Start the server.
- server.play();
+ // Start the server.
+ server.start();
- // Ask the server for its URL. You'll need this to make HTTP requests.
- URL baseUrl = server.getUrl("/v1/chat/");
+ // Ask the server for its URL. You'll need this to make HTTP requests.
+ URL baseUrl = server.getUrl("/v1/chat/");
- // Exercise your application code, which should make those HTTP requests.
- // Responses are returned in the same order that they are enqueued.
- Chat chat = new Chat(baseUrl);
+ // Exercise your application code, which should make those HTTP requests.
+ // Responses are returned in the same order that they are enqueued.
+ Chat chat = new Chat(baseUrl);
- chat.loadMore();
- assertEquals("hello, world!", chat.messages());
+ chat.loadMore();
+ assertEquals("hello, world!", chat.messages());
- chat.loadMore();
- chat.loadMore();
- assertEquals(""
- + "hello, world!\n"
- + "sup, bra?\n"
- + "yo dog", chat.messages());
+ chat.loadMore();
+ chat.loadMore();
+ assertEquals(""
+ + "hello, world!\n"
+ + "sup, bra?\n"
+ + "yo dog", chat.messages());
- // Optional: confirm that your app made the HTTP requests you were expecting.
- RecordedRequest request1 = server.takeRequest();
- assertEquals("/v1/chat/messages/", request1.getPath());
- assertNotNull(request1.getHeader("Authorization"));
+ // Optional: confirm that your app made the HTTP requests you were expecting.
+ RecordedRequest request1 = server.takeRequest();
+ assertEquals("/v1/chat/messages/", request1.getPath());
+ assertNotNull(request1.getHeader("Authorization"));
- RecordedRequest request2 = server.takeRequest();
- assertEquals("/v1/chat/messages/2", request2.getPath());
+ RecordedRequest request2 = server.takeRequest();
+ assertEquals("/v1/chat/messages/2", request2.getPath());
- RecordedRequest request3 = server.takeRequest();
- assertEquals("/v1/chat/messages/3", request3.getPath());
+ RecordedRequest request3 = server.takeRequest();
+ assertEquals("/v1/chat/messages/3", request3.getPath());
- // Shut down the server. Instances cannot be reused.
- server.shutdown();
- }
+ // Shut down the server. Instances cannot be reused.
+ server.shutdown();
+}
```
Your unit tests might move the `server` into a field so you can shut it down
@@ -85,18 +85,18 @@
You can set a custom body with a string, input stream or byte array. Also
add headers with a fluent builder API.
-```
- MockResponse response = new MockResponse()
- .addHeader("Content-Type", "application/json; charset=utf-8")
- .addHeader("Cache-Control", "no-cache")
- .setBody("{}");
+```java
+MockResponse response = new MockResponse()
+ .addHeader("Content-Type", "application/json; charset=utf-8")
+ .addHeader("Cache-Control", "no-cache")
+ .setBody("{}");
```
MockResponse can be used to simulate a slow network. This is useful for
testing timeouts and interactive testing.
-```
- response.throttleBody(1024, 1, TimeUnit.SECONDS);
+```java
+response.throttleBody(1024, 1, TimeUnit.SECONDS);
```
@@ -104,11 +104,11 @@
Verify requests by their method, path, HTTP version, body, and headers.
-```
- RecordedRequest request = server.takeRequest();
- assertEquals("POST /v1/chat/send HTTP/1.1", request.getRequestLine());
- assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
- assertEquals("{}", request.getUtf8Body());
+```java
+RecordedRequest request = server.takeRequest();
+assertEquals("POST /v1/chat/send HTTP/1.1", request.getRequestLine());
+assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
+assertEquals("{}", request.getUtf8Body());
```
#### Dispatcher
@@ -122,7 +122,7 @@
The best way to get MockWebServer is via Maven:
-```
+```xml
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>mockwebserver</artifactId>
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
index 9c7af9d..ae3abb5 100644
--- a/mockwebserver/pom.xml
+++ b/mockwebserver/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>mockwebserver</artifactId>
@@ -23,14 +23,9 @@
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
- <groupId>org.mortbay.jetty.npn</groupId>
- <artifactId>npn-boot</artifactId>
- <optional>true</optional>
- </dependency>
- <dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
- <scope>test</scope>
+ <optional>true</optional>
</dependency>
</dependencies>
@@ -38,6 +33,16 @@
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <links>
+ <link>http://square.github.io/okhttp/javadoc/</link>
+ <link>http://square.github.io/okio/</link>
+ </links>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptorRefs>
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 253fcbd..3d61d73 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -68,7 +68,7 @@
public static synchronized SSLContext localhost() {
if (localhost == null) {
try {
- localhost = new SslContextBuilder(InetAddress.getLocalHost().getHostName()).build();
+ localhost = new SslContextBuilder(InetAddress.getByName(null).getHostName()).build();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (UnknownHostException e) {
@@ -83,7 +83,7 @@
// Generate public and private keys and use them to make a self-signed certificate.
KeyPair keyPair = generateKeyPair();
- X509Certificate certificate = selfSignedCertificate(keyPair);
+ X509Certificate certificate = selfSignedCertificate(keyPair, "1");
// Put 'em in a key store.
KeyStore keyStore = newEmptyKeyStore(password);
@@ -104,7 +104,7 @@
return sslContext;
}
- private KeyPair generateKeyPair() throws GeneralSecurityException {
+ public KeyPair generateKeyPair() throws GeneralSecurityException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
keyPairGenerator.initialize(1024, new SecureRandom());
return keyPairGenerator.generateKeyPair();
@@ -115,11 +115,12 @@
* public key, signed by {@code keyPair}'s private key.
*/
@SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
- private X509Certificate selfSignedCertificate(KeyPair keyPair) throws GeneralSecurityException {
+ 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(BigInteger.ONE);
+ generator.setSerialNumber(new BigInteger(serialNumber));
generator.setIssuerDN(issuer);
generator.setNotBefore(new Date(notBefore));
generator.setNotAfter(new Date(notAfter));
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
index 098f3c9..fb21a08 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/spdy/SpdyServer.java
@@ -17,12 +17,11 @@
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import java.io.File;
-import java.io.FileInputStream;
import java.io.IOException;
-import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Arrays;
@@ -31,14 +30,15 @@
import javax.net.ssl.SSLSocketFactory;
import okio.BufferedSink;
import okio.Okio;
-import org.eclipse.jetty.npn.NextProtoNego;
+import okio.Source;
-import static com.squareup.okhttp.internal.Util.headerEntries;
-
-/** A basic SPDY server that serves the contents of a local directory. */
+/** A basic SPDY/HTTP_2 server that serves the contents of a local directory. */
public final class SpdyServer implements IncomingStreamHandler {
+ private final List<Protocol> spdyProtocols = Util.immutableList(Protocol.HTTP_2, Protocol.SPDY_3);
+
private final File baseDirectory;
private SSLSocketFactory sslSocketFactory;
+ private Protocol protocol;
public SpdyServer(File baseDirectory) {
this.baseDirectory = baseDirectory;
@@ -57,7 +57,7 @@
if (sslSocketFactory != null) {
socket = doSsl(socket);
}
- new SpdyConnection.Builder(false, socket).handler(this).build();
+ new SpdyConnection.Builder(false, socket).protocol(protocol).handler(this).build();
}
}
@@ -66,24 +66,20 @@
(SSLSocket) sslSocketFactory.createSocket(socket, socket.getInetAddress().getHostAddress(),
socket.getPort(), true);
sslSocket.setUseClientMode(false);
- NextProtoNego.put(sslSocket, new NextProtoNego.ServerProvider() {
- @Override public void unsupported() {
- System.out.println("UNSUPPORTED");
- }
- @Override public List<String> protocols() {
- return Arrays.asList(Protocol.SPDY_3.name.utf8());
- }
- @Override public void protocolSelected(String protocol) {
- System.out.println("PROTOCOL SELECTED: " + protocol);
- }
- });
+ Platform.get().configureTlsExtensions(sslSocket, null, spdyProtocols);
+ sslSocket.startHandshake();
+ String protocolString = Platform.get().getSelectedProtocol(sslSocket);
+ protocol = protocolString != null ? Protocol.get(protocolString) : null;
+ if (protocol == null || !spdyProtocols.contains(protocol)) {
+ throw new IllegalStateException("Protocol " + protocol + " unsupported");
+ }
return sslSocket;
}
@Override public void receive(final SpdyStream stream) throws IOException {
List<Header> requestHeaders = stream.getRequestHeaders();
String path = null;
- for (int i = 0; i < requestHeaders.size(); i++) {
+ for (int i = 0, size = requestHeaders.size(); i < size; i++) {
if (requestHeaders.get(i).name.equals(Header.TARGET_PATH)) {
path = requestHeaders.get(i).value.utf8();
break;
@@ -107,8 +103,11 @@
}
private void send404(SpdyStream stream, String path) throws IOException {
- List<Header> responseHeaders =
- headerEntries(":status", "404", ":version", "HTTP/1.1", "content-type", "text/plain");
+ List<Header> responseHeaders = Arrays.asList(
+ new Header(":status", "404"),
+ new Header(":version", "HTTP/1.1"),
+ new Header("content-type", "text/plain")
+ );
stream.reply(responseHeaders, true);
BufferedSink out = Okio.buffer(stream.getSink());
out.writeUtf8("Not found: " + path);
@@ -116,9 +115,11 @@
}
private void serveDirectory(SpdyStream stream, String[] files) throws IOException {
- List<Header> responseHeaders =
- headerEntries(":status", "200", ":version", "HTTP/1.1", "content-type",
- "text/html; charset=UTF-8");
+ List<Header> responseHeaders = Arrays.asList(
+ new Header(":status", "200"),
+ new Header(":version", "HTTP/1.1"),
+ new Header("content-type", "text/html; charset=UTF-8")
+ );
stream.reply(responseHeaders, true);
BufferedSink out = Okio.buffer(stream.getSink());
for (String file : files) {
@@ -128,20 +129,19 @@
}
private void serveFile(SpdyStream stream, File file) throws IOException {
- byte[] buffer = new byte[8192];
- stream.reply(
- headerEntries(":status", "200", ":version", "HTTP/1.1", "content-type", contentType(file)),
- true);
- InputStream in = new FileInputStream(file);
- BufferedSink out = Okio.buffer(stream.getSink());
+ List<Header> responseHeaders = Arrays.asList(
+ new Header(":status", "200"),
+ new Header(":version", "HTTP/1.1"),
+ new Header("content-type", contentType(file))
+ );
+ stream.reply(responseHeaders, true);
+ Source source = Okio.source(file);
try {
- int count;
- while ((count = in.read(buffer)) != -1) {
- out.write(buffer, 0, count);
- }
+ BufferedSink out = Okio.buffer(stream.getSink());
+ out.writeAll(source);
+ out.close();
} finally {
- Util.closeQuietly(in);
- Util.closeQuietly(out);
+ Util.closeQuietly(source);
}
}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
index b7de9b6..4e1e0e7 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/Dispatcher.java
@@ -33,9 +33,4 @@
public MockResponse peek() {
return new MockResponse().setSocketPolicy(SocketPolicy.KEEP_OPEN);
}
-
- /** @deprecated replaced with {@link #peek}. */
- protected final SocketPolicy peekSocketPolicy() {
- throw new UnsupportedOperationException("This API is obsolete. Override peek() instead!");
- }
}
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 7d8e066..8f0ee2c 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -15,49 +15,44 @@
*/
package com.squareup.okhttp.mockwebserver;
-import com.squareup.okhttp.internal.Util;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.internal.ws.WebSocketListener;
import java.util.ArrayList;
-import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
+import okio.Buffer;
/** A scripted response to be replayed by the mock web server. */
public final class MockResponse implements Cloneable {
private static final String CHUNKED_BODY_HEADER = "Transfer-encoding: chunked";
private String status = "HTTP/1.1 200 OK";
- private List<String> headers = new ArrayList<String>();
+ private Headers.Builder headers = new Headers.Builder();
- /** The response body content, or null if {@code bodyStream} is set. */
- private byte[] body;
- /** The response body content, or null if {@code body} is set. */
- private InputStream bodyStream;
+ private Buffer body;
- private int throttleBytesPerPeriod = Integer.MAX_VALUE;
- private long throttlePeriod = 1;
- private TimeUnit throttleUnit = TimeUnit.SECONDS;
+ private long throttleBytesPerPeriod = Long.MAX_VALUE;
+ private long throttlePeriodAmount = 1;
+ private TimeUnit throttlePeriodUnit = TimeUnit.SECONDS;
private SocketPolicy socketPolicy = SocketPolicy.KEEP_OPEN;
- private int bodyDelayTimeMs = 0;
+ private long bodyDelayAmount = 0;
+ private TimeUnit bodyDelayUnit = TimeUnit.MILLISECONDS;
- private List<PushPromise> promises = new ArrayList<PushPromise>();
+ private List<PushPromise> promises = new ArrayList<>();
+ private WebSocketListener webSocketListener;
/** Creates a new mock response with an empty body. */
public MockResponse() {
- setBody(new byte[0]);
+ setHeader("Content-Length", 0);
}
@Override public MockResponse clone() {
try {
MockResponse result = (MockResponse) super.clone();
- result.headers = new ArrayList<String>(headers);
- result.promises = new ArrayList<PushPromise>(promises);
+ result.headers = headers.build().newBuilder();
+ result.promises = new ArrayList<>(promises);
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
@@ -70,8 +65,7 @@
}
public MockResponse setResponseCode(int code) {
- this.status = "HTTP/1.1 " + code + " OK";
- return this;
+ return setStatus("HTTP/1.1 " + code + " OK");
}
public MockResponse setStatus(String status) {
@@ -80,8 +74,8 @@
}
/** Returns the HTTP headers, such as "Content-Length: 0". */
- public List<String> getHeaders() {
- return headers;
+ public Headers getHeaders() {
+ return headers.build();
}
/**
@@ -89,7 +83,7 @@
* "Transfer-encoding" headers that were added by default.
*/
public MockResponse clearHeaders() {
- headers.clear();
+ headers = new Headers.Builder();
return this;
}
@@ -107,7 +101,8 @@
* headers with the same name.
*/
public MockResponse addHeader(String name, Object value) {
- return addHeader(name + ": " + String.valueOf(value));
+ headers.add(name, String.valueOf(value));
+ return this;
}
/**
@@ -119,77 +114,54 @@
return addHeader(name, value);
}
+ /** Replaces all headers with those specified in {@code headers}. */
+ public MockResponse setHeaders(Headers headers) {
+ this.headers = headers.newBuilder();
+ return this;
+ }
+
/** Removes all headers named {@code name}. */
public MockResponse removeHeader(String name) {
- name += ":";
- for (Iterator<String> i = headers.iterator(); i.hasNext(); ) {
- String header = i.next();
- if (name.regionMatches(true, 0, header, 0, name.length())) {
- i.remove();
- }
- }
+ headers.removeAll(name);
return this;
}
- /** Returns the raw HTTP payload, or null if this response is streamed. */
- public byte[] getBody() {
- return body;
+ /** Returns a copy of the raw HTTP payload. */
+ public Buffer getBody() {
+ return body != null ? body.clone() : null;
}
- /** Returns an input stream containing the raw HTTP payload. */
- InputStream getBodyStream() {
- return bodyStream != null ? bodyStream : new ByteArrayInputStream(body);
- }
-
- public MockResponse setBody(byte[] body) {
- setHeader("Content-Length", body.length);
- this.body = body;
- this.bodyStream = null;
- return this;
- }
-
- public MockResponse setBody(InputStream bodyStream, long bodyLength) {
- setHeader("Content-Length", bodyLength);
- this.body = null;
- this.bodyStream = bodyStream;
+ public MockResponse setBody(Buffer body) {
+ setHeader("Content-Length", body.size());
+ this.body = body.clone(); // Defensive copy.
return this;
}
/** Sets the response body to the UTF-8 encoded bytes of {@code body}. */
public MockResponse setBody(String body) {
- try {
- return setBody(body.getBytes("UTF-8"));
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError();
- }
+ return setBody(new Buffer().writeUtf8(body));
}
/**
* Sets the response body to {@code body}, chunked every {@code maxChunkSize}
* bytes.
*/
- public MockResponse setChunkedBody(byte[] body, int maxChunkSize) {
+ public MockResponse setChunkedBody(Buffer body, int maxChunkSize) {
removeHeader("Content-Length");
headers.add(CHUNKED_BODY_HEADER);
- try {
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- int pos = 0;
- while (pos < body.length) {
- int chunkSize = Math.min(body.length - pos, maxChunkSize);
- bytesOut.write(Integer.toHexString(chunkSize).getBytes(Util.US_ASCII));
- bytesOut.write("\r\n".getBytes(Util.US_ASCII));
- bytesOut.write(body, pos, chunkSize);
- bytesOut.write("\r\n".getBytes(Util.US_ASCII));
- pos += chunkSize;
- }
- bytesOut.write("0\r\n\r\n".getBytes(Util.US_ASCII)); // Last chunk + empty trailer + crlf.
-
- this.body = bytesOut.toByteArray();
- return this;
- } catch (IOException e) {
- throw new AssertionError(); // In-memory I/O doesn't throw IOExceptions.
+ Buffer bytesOut = new Buffer();
+ while (!body.exhausted()) {
+ long chunkSize = Math.min(body.size(), maxChunkSize);
+ bytesOut.writeUtf8(Long.toHexString(chunkSize));
+ bytesOut.writeUtf8("\r\n");
+ bytesOut.write(body, chunkSize);
+ bytesOut.writeUtf8("\r\n");
}
+ bytesOut.writeUtf8("0\r\n\r\n"); // Last chunk + empty trailer + CRLF.
+
+ this.body = bytesOut;
+ return this;
}
/**
@@ -197,11 +169,7 @@
* every {@code maxChunkSize} bytes.
*/
public MockResponse setChunkedBody(String body, int maxChunkSize) {
- try {
- return setChunkedBody(body.getBytes("UTF-8"), maxChunkSize);
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError();
- }
+ return setChunkedBody(new Buffer().writeUtf8(body), maxChunkSize);
}
public SocketPolicy getSocketPolicy() {
@@ -218,41 +186,39 @@
* series of {@code bytesPerPeriod} bytes are written. Use this to simulate
* network behavior.
*/
- public MockResponse throttleBody(int bytesPerPeriod, long period, TimeUnit unit) {
+ public MockResponse throttleBody(long bytesPerPeriod, long period, TimeUnit unit) {
this.throttleBytesPerPeriod = bytesPerPeriod;
- this.throttlePeriod = period;
- this.throttleUnit = unit;
+ this.throttlePeriodAmount = period;
+ this.throttlePeriodUnit = unit;
return this;
}
- public int getThrottleBytesPerPeriod() {
+ public long getThrottleBytesPerPeriod() {
return throttleBytesPerPeriod;
}
- public long getThrottlePeriod() {
- return throttlePeriod;
- }
-
- public TimeUnit getThrottleUnit() {
- return throttleUnit;
+ public long getThrottlePeriod(TimeUnit unit) {
+ return unit.convert(throttlePeriodAmount, throttlePeriodUnit);
}
/**
* Set the delayed time of the response body to {@code delay}. This applies to the
* response body only; response headers are not affected.
*/
- public MockResponse setBodyDelayTimeMs(int delay) {
- bodyDelayTimeMs = delay;
+ public MockResponse setBodyDelay(long delay, TimeUnit unit) {
+ bodyDelayAmount = delay;
+ bodyDelayUnit = unit;
return this;
}
- public int getBodyDelayTimeMs() {
- return bodyDelayTimeMs;
+ public long getBodyDelay(TimeUnit unit) {
+ return unit.convert(bodyDelayAmount, bodyDelayUnit);
}
/**
- * When {@link MockWebServer#setNpnProtocols(java.util.List) protocols}
- * include a SPDY variant, this attaches a pushed stream to this response.
+ * When {@link MockWebServer#setProtocols(java.util.List) protocols}
+ * include {@linkplain com.squareup.okhttp.Protocol#HTTP_2}, this attaches a
+ * pushed stream to this response.
*/
public MockResponse withPush(PushPromise promise) {
this.promises.add(promise);
@@ -264,6 +230,23 @@
return promises;
}
+ /**
+ * Attempts to perform a web socket upgrade on the connection. This will overwrite any previously
+ * set status or body.
+ */
+ public MockResponse withWebSocketUpgrade(WebSocketListener listener) {
+ setStatus("HTTP/1.1 101 Switching Protocols");
+ setHeader("Connection", "Upgrade");
+ setHeader("Upgrade", "websocket");
+ body = null;
+ webSocketListener = listener;
+ return this;
+ }
+
+ public WebSocketListener getWebSocketListener() {
+ return webSocketListener;
+ }
+
@Override public String toString() {
return status;
}
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 5a8357b..a63bbd4 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -17,7 +17,10 @@
package com.squareup.okhttp.mockwebserver;
+import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.NamedRunnable;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
@@ -26,21 +29,19 @@
import com.squareup.okhttp.internal.spdy.IncomingStreamHandler;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import com.squareup.okhttp.internal.spdy.SpdyStream;
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
+import com.squareup.okhttp.internal.ws.RealWebSocket;
+import com.squareup.okhttp.internal.ws.WebSocketListener;
+import com.squareup.okhttp.internal.ws.WebSocketProtocol;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
+import java.net.ProtocolException;
import java.net.Proxy;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
-import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
@@ -49,24 +50,30 @@
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
-import java.util.Map;
+import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
+import javax.net.ServerSocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
+import okio.Buffer;
import okio.BufferedSink;
+import okio.BufferedSource;
import okio.ByteString;
-import okio.OkBuffer;
import okio.Okio;
+import okio.Sink;
+import okio.Timeout;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
@@ -93,15 +100,15 @@
private static final Logger logger = Logger.getLogger(MockWebServer.class.getName());
- private final BlockingQueue<RecordedRequest> requestQueue =
- new LinkedBlockingQueue<RecordedRequest>();
+ private final BlockingQueue<RecordedRequest> requestQueue = new LinkedBlockingQueue<>();
- /** All map values are Boolean.TRUE. (Collections.newSetFromMap isn't available in Froyo) */
- private final Map<Socket, Boolean> openClientSockets = new ConcurrentHashMap<Socket, Boolean>();
- private final Map<SpdyConnection, Boolean> openSpdyConnections
- = new ConcurrentHashMap<SpdyConnection, Boolean>();
+ private final Set<Socket> openClientSockets =
+ Collections.newSetFromMap(new ConcurrentHashMap<Socket, Boolean>());
+ private final Set<SpdyConnection> openSpdyConnections =
+ Collections.newSetFromMap(new ConcurrentHashMap<SpdyConnection, Boolean>());
private final AtomicInteger requestCount = new AtomicInteger();
- private int bodyLimit = Integer.MAX_VALUE;
+ private long bodyLimit = Long.MAX_VALUE;
+ private ServerSocketFactory serverSocketFactory = ServerSocketFactory.getDefault();
private ServerSocket serverSocket;
private SSLSocketFactory sslSocketFactory;
private ExecutorService executor;
@@ -109,24 +116,31 @@
private Dispatcher dispatcher = new QueueDispatcher();
private int port = -1;
- private boolean npnEnabled = true;
- private List<Protocol> npnProtocols = Protocol.HTTP2_SPDY3_AND_HTTP;
+ private InetAddress inetAddress;
+ private boolean protocolNegotiationEnabled = true;
+ private List<Protocol> protocols
+ = Util.immutableList(Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1);
+
+ public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
+ if (serverSocketFactory == null) throw new IllegalArgumentException("null serverSocketFactory");
+ this.serverSocketFactory = serverSocketFactory;
+ }
public int getPort() {
- if (port == -1) throw new IllegalStateException("Cannot retrieve port before calling play()");
+ if (port == -1) throw new IllegalStateException("Call start() before getPort()");
return port;
}
public String getHostName() {
- try {
- return InetAddress.getLocalHost().getHostName();
- } catch (UnknownHostException e) {
- throw new AssertionError(e);
- }
+ if (inetAddress == null) throw new IllegalStateException("Call start() before getHostName()");
+ return inetAddress.getHostName();
}
public Proxy toProxyAddress() {
- return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(getHostName(), getPort()));
+ if (inetAddress == null) {
+ throw new IllegalStateException("Call start() before toProxyAddress()");
+ }
+ return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(inetAddress, getPort()));
}
/**
@@ -157,35 +171,36 @@
* Sets the number of bytes of the POST body to keep in memory to the given
* limit.
*/
- public void setBodyLimit(int maxBodyLength) {
+ public void setBodyLimit(long maxBodyLength) {
this.bodyLimit = maxBodyLength;
}
/**
- * Sets whether NPN is used on incoming HTTPS connections to negotiate a
- * protocol like HTTP/1.1 or SPDY/3. Call this method to disable NPN and
- * SPDY.
+ * Sets whether ALPN is used on incoming HTTPS connections to
+ * negotiate a protocol like HTTP/1.1 or HTTP/2. Call this method to disable
+ * negotiation and restrict connections to HTTP/1.1.
*/
- public void setNpnEnabled(boolean npnEnabled) {
- this.npnEnabled = npnEnabled;
+ public void setProtocolNegotiationEnabled(boolean protocolNegotiationEnabled) {
+ this.protocolNegotiationEnabled = protocolNegotiationEnabled;
}
/**
- * Indicates the protocols supported by NPN on incoming HTTPS connections.
- * This list is ignored when npn is disabled.
+ * Indicates the protocols supported by ALPN on incoming HTTPS
+ * connections. This list is ignored when
+ * {@link #setProtocolNegotiationEnabled negotiation is disabled}.
*
* @param protocols the protocols to use, in order of preference. The list
- * must contain "http/1.1". It must not contain null.
+ * must contain {@linkplain Protocol#HTTP_1_1}. It must not contain null.
*/
- public void setNpnProtocols(List<Protocol> protocols) {
+ public void setProtocols(List<Protocol> protocols) {
protocols = Util.immutableList(protocols);
- if (!protocols.contains(Protocol.HTTP_11)) {
+ if (!protocols.contains(Protocol.HTTP_1_1)) {
throw new IllegalArgumentException("protocols doesn't contain http/1.1: " + protocols);
}
if (protocols.contains(null)) {
throw new IllegalArgumentException("protocols must not contain null");
}
- this.npnProtocols = Util.immutableList(protocols);
+ this.protocols = protocols;
}
/**
@@ -200,13 +215,31 @@
/**
* Awaits the next HTTP request, removes it, and returns it. Callers should
- * use this to verify the request was sent as intended.
+ * use this to verify the request was sent as intended. This method will block until the
+ * request is available, possibly forever.
+ *
+ * @return the head of the request queue
*/
public RecordedRequest takeRequest() throws InterruptedException {
return requestQueue.take();
}
/**
+ * Awaits the next HTTP request (waiting up to the
+ * specified wait time if necessary), removes it, and returns it. Callers should
+ * use this to verify the request was sent as intended within the given time.
+ *
+ * @param timeout how long to wait before giving up, in units of
+ * {@code unit}
+ * @param unit a {@code TimeUnit} determining how to interpret the
+ * {@code timeout} parameter
+ * @return the head of the request queue
+ */
+ public RecordedRequest takeRequest(long timeout, TimeUnit unit) throws InterruptedException {
+ return requestQueue.poll(timeout, unit);
+ }
+
+ /**
* Returns the number of HTTP requests received thus far by this server. This
* may exceed the number of HTTP connections when connection reuse is in
* practice.
@@ -227,41 +260,53 @@
((QueueDispatcher) dispatcher).enqueueResponse(response.clone());
}
- /** Equivalent to {@code play(0)}. */
+ /** @deprecated Use {@link #start()}. */
public void play() throws IOException {
- play(0);
+ start();
+ }
+
+ /** @deprecated Use {@link #start(int)}. */
+ public void play(int port) throws IOException {
+ start(port);
+ }
+
+ /** Equivalent to {@code start(0)}. */
+ public void start() throws IOException {
+ start(0);
}
/**
- * Starts the server, serves all enqueued requests, and shuts the server down.
+ * Starts the server.
*
* @param port the port to listen to, or 0 for any available port. Automated
* tests should always use port 0 to avoid flakiness when a specific port
* is unavailable.
*/
- public void play(int port) throws IOException {
- if (executor != null) throw new IllegalStateException("play() already called");
+ public void start(int port) throws IOException {
+ if (executor != null) throw new IllegalStateException("start() already called");
executor = Executors.newCachedThreadPool(Util.threadFactory("MockWebServer", false));
- serverSocket = new ServerSocket(port);
- serverSocket.setReuseAddress(true);
+ inetAddress = InetAddress.getByName("localhost");
+ serverSocket = serverSocketFactory.createServerSocket();
+ serverSocket.setReuseAddress(port != 0); // Reuse the port if the port number was specified.
+ serverSocket.bind(new InetSocketAddress(inetAddress, port), 50);
this.port = serverSocket.getLocalPort();
- executor.execute(new NamedRunnable("MockWebServer %s", port) {
+ executor.execute(new NamedRunnable("MockWebServer %s", this.port) {
@Override protected void execute() {
try {
+ logger.info(MockWebServer.this + " starting to accept connections");
acceptConnections();
} catch (Throwable e) {
- logger.log(Level.WARNING, "MockWebServer connection failed", e);
+ logger.log(Level.WARNING, MockWebServer.this + " failed unexpectedly", e);
}
- // This gnarly block of code will release all sockets and all thread,
- // even if any close fails.
+ // Release all sockets and all threads, even if any close fails.
Util.closeQuietly(serverSocket);
- for (Iterator<Socket> s = openClientSockets.keySet().iterator(); s.hasNext(); ) {
+ for (Iterator<Socket> s = openClientSockets.iterator(); s.hasNext(); ) {
Util.closeQuietly(s.next());
s.remove();
}
- for (Iterator<SpdyConnection> s = openSpdyConnections.keySet().iterator(); s.hasNext(); ) {
+ for (Iterator<SpdyConnection> s = openSpdyConnections.iterator(); s.hasNext(); ) {
Util.closeQuietly(s.next());
s.remove();
}
@@ -274,6 +319,7 @@
try {
socket = serverSocket.accept();
} catch (SocketException e) {
+ logger.info(MockWebServer.this + " done accepting connections: " + e.getMessage());
return;
}
SocketPolicy socketPolicy = dispatcher.peek().getSocketPolicy();
@@ -281,7 +327,7 @@
dispatchBookkeepingRequest(0, socket);
socket.close();
} else {
- openClientSockets.put(socket, true);
+ openClientSockets.add(socket);
serveConnection(socket);
}
}
@@ -290,8 +336,16 @@
}
public void shutdown() throws IOException {
- if (serverSocket != null) {
- serverSocket.close(); // Should cause acceptConnections() to break out.
+ // Cause acceptConnections() to break out.
+ serverSocket.close();
+
+ // Await shutdown.
+ try {
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ throw new IOException("Gave up waiting for executor to shut down");
+ }
+ } catch (InterruptedException e) {
+ throw new AssertionError();
}
}
@@ -302,13 +356,17 @@
@Override protected void execute() {
try {
processConnection();
+ } catch (IOException e) {
+ logger.info(
+ MockWebServer.this + " connection from " + raw.getInetAddress() + " failed: " + e);
} catch (Exception e) {
- logger.log(Level.WARNING, "MockWebServer connection failed", e);
+ logger.log(Level.SEVERE,
+ MockWebServer.this + " connection from " + raw.getInetAddress() + " crashed", e);
}
}
public void processConnection() throws Exception {
- Protocol protocol = Protocol.HTTP_11;
+ Protocol protocol = Protocol.HTTP_1_1;
Socket socket;
if (sslSocketFactory != null) {
if (tunnelProxy) {
@@ -320,49 +378,53 @@
processHandshakeFailure(raw);
return;
}
- socket = sslSocketFactory.createSocket(
- raw, raw.getInetAddress().getHostAddress(), raw.getPort(), true);
+ socket = sslSocketFactory.createSocket(raw, raw.getInetAddress().getHostAddress(),
+ raw.getPort(), true);
SSLSocket sslSocket = (SSLSocket) socket;
sslSocket.setUseClientMode(false);
- openClientSockets.put(socket, true);
+ openClientSockets.add(socket);
- if (npnEnabled) {
- Platform.get().setNpnProtocols(sslSocket, npnProtocols);
+ if (protocolNegotiationEnabled) {
+ Platform.get().configureTlsExtensions(sslSocket, null, protocols);
}
sslSocket.startHandshake();
- if (npnEnabled) {
- ByteString selectedProtocol = Platform.get().getNpnSelectedProtocol(sslSocket);
- protocol = Protocol.find(selectedProtocol);
+ if (protocolNegotiationEnabled) {
+ String protocolString = Platform.get().getSelectedProtocol(sslSocket);
+ protocol = protocolString != null ? Protocol.get(protocolString) : Protocol.HTTP_1_1;
}
openClientSockets.remove(raw);
} else {
socket = raw;
}
- if (protocol.spdyVariant) {
+ if (protocol != Protocol.HTTP_1_1) {
SpdySocketHandler spdySocketHandler = new SpdySocketHandler(socket, protocol);
- SpdyConnection spdyConnection = new SpdyConnection.Builder(false, socket)
- .protocol(protocol)
- .handler(spdySocketHandler).build();
- openSpdyConnections.put(spdyConnection, Boolean.TRUE);
+ SpdyConnection spdyConnection =
+ new SpdyConnection.Builder(false, socket).protocol(protocol)
+ .handler(spdySocketHandler)
+ .build();
+ openSpdyConnections.add(spdyConnection);
openClientSockets.remove(socket);
return;
}
- InputStream in = new BufferedInputStream(socket.getInputStream());
- OutputStream out = new BufferedOutputStream(socket.getOutputStream());
+ BufferedSource source = Okio.buffer(Okio.source(socket));
+ BufferedSink sink = Okio.buffer(Okio.sink(socket));
- while (processOneRequest(socket, in, out)) {
+ while (processOneRequest(socket, source, sink)) {
}
if (sequenceNumber == 0) {
- logger.warning("MockWebServer connection didn't make a request");
+ logger.warning(MockWebServer.this
+ + " connection from "
+ + raw.getInetAddress()
+ + " didn't make a request");
}
- in.close();
- out.close();
+ source.close();
+ sink.close();
socket.close();
openClientSockets.remove(socket);
}
@@ -372,9 +434,11 @@
* dispatched.
*/
private void createTunnel() throws IOException, InterruptedException {
+ BufferedSource source = Okio.buffer(Okio.source(raw));
+ BufferedSink sink = Okio.buffer(Okio.sink(raw));
while (true) {
SocketPolicy socketPolicy = dispatcher.peek().getSocketPolicy();
- if (!processOneRequest(raw, raw.getInputStream(), raw.getOutputStream())) {
+ if (!processOneRequest(raw, source, sink)) {
throw new IllegalStateException("Tunnel without any CONNECT!");
}
if (socketPolicy == SocketPolicy.UPGRADE_TO_SSL_AT_END) return;
@@ -385,25 +449,50 @@
* Reads a request and writes its response. Returns true if a request was
* processed.
*/
- private boolean processOneRequest(Socket socket, InputStream in, OutputStream out)
+ private boolean processOneRequest(Socket socket, BufferedSource source, BufferedSink sink)
throws IOException, InterruptedException {
- RecordedRequest request = readRequest(socket, in, out, sequenceNumber);
+ RecordedRequest request = readRequest(socket, source, sink, sequenceNumber);
if (request == null) return false;
+
requestCount.incrementAndGet();
requestQueue.add(request);
+
MockResponse response = dispatcher.dispatch(request);
- writeResponse(out, response);
+ if (response.getSocketPolicy() == SocketPolicy.DISCONNECT_AFTER_REQUEST) {
+ socket.close();
+ return false;
+ }
+ if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
+ // This read should block until the socket is closed. (Because nobody is writing.)
+ if (source.exhausted()) return false;
+ throw new ProtocolException("unexpected data");
+ }
+
+ boolean requestWantsWebSockets = "Upgrade".equalsIgnoreCase(request.getHeader("Connection"))
+ && "websocket".equalsIgnoreCase(request.getHeader("Upgrade"));
+ boolean responseWantsWebSockets = response.getWebSocketListener() != null;
+ if (requestWantsWebSockets && responseWantsWebSockets) {
+ handleWebSocketUpgrade(socket, source, sink, request, response);
+ } else {
+ writeHttpResponse(socket, sink, response);
+ }
+
if (response.getSocketPolicy() == SocketPolicy.DISCONNECT_AT_END) {
- in.close();
- out.close();
+ source.close();
+ sink.close();
+ // Workaround for bug on Android: closing the input/output streams should close an
+ // SSLSocket but does not. https://code.google.com/p/android/issues/detail?id=97564
+ socket.close();
} else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_INPUT_AT_END) {
socket.shutdownInput();
} else if (response.getSocketPolicy() == SocketPolicy.SHUTDOWN_OUTPUT_AT_END) {
socket.shutdownOutput();
}
if (logger.isLoggable(Level.INFO)) {
- logger.info("Received request: " + request + " and responded: " + response);
+ logger.info(
+ MockWebServer.this + " received request: " + request + " and responded: " + response);
}
+
sequenceNumber++;
return true;
}
@@ -431,11 +520,11 @@
}
/** @param sequenceNumber the index of this request on this connection. */
- private RecordedRequest readRequest(Socket socket, InputStream in, OutputStream out,
+ private RecordedRequest readRequest(Socket socket, BufferedSource source, BufferedSink sink,
int sequenceNumber) throws IOException {
String request;
try {
- request = readAsciiUntilCrlf(in);
+ request = source.readUtf8LineStrict();
} catch (IOException streamIsClosed) {
return null; // no request because we closed the stream
}
@@ -443,12 +532,12 @@
return null; // no request because the stream is exhausted
}
- List<String> headers = new ArrayList<String>();
+ Headers.Builder headers = new Headers.Builder();
long contentLength = -1;
boolean chunked = false;
boolean expectContinue = false;
String header;
- while ((header = readAsciiUntilCrlf(in)).length() != 0) {
+ while ((header = source.readUtf8LineStrict()).length() != 0) {
headers.add(header);
String lowercaseHeader = header.toLowerCase(Locale.US);
if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) {
@@ -465,30 +554,30 @@
}
if (expectContinue) {
- out.write(("HTTP/1.1 100 Continue\r\n").getBytes(Util.US_ASCII));
- out.write(("Content-Length: 0\r\n").getBytes(Util.US_ASCII));
- out.write(("\r\n").getBytes(Util.US_ASCII));
- out.flush();
+ sink.writeUtf8("HTTP/1.1 100 Continue\r\n");
+ sink.writeUtf8("Content-Length: 0\r\n");
+ sink.writeUtf8("\r\n");
+ sink.flush();
}
boolean hasBody = false;
- TruncatingOutputStream requestBody = new TruncatingOutputStream();
- List<Integer> chunkSizes = new ArrayList<Integer>();
+ TruncatingBuffer requestBody = new TruncatingBuffer(bodyLimit);
+ List<Integer> chunkSizes = new ArrayList<>();
MockResponse throttlePolicy = dispatcher.peek();
if (contentLength != -1) {
- hasBody = true;
- throttledTransfer(throttlePolicy, in, requestBody, contentLength);
+ hasBody = contentLength > 0;
+ throttledTransfer(throttlePolicy, socket, source, Okio.buffer(requestBody), contentLength);
} else if (chunked) {
hasBody = true;
while (true) {
- int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16);
+ int chunkSize = Integer.parseInt(source.readUtf8LineStrict().trim(), 16);
if (chunkSize == 0) {
- readEmptyLine(in);
+ readEmptyLine(source);
break;
}
chunkSizes.add(chunkSize);
- throttledTransfer(throttlePolicy, in, requestBody, chunkSize);
- readEmptyLine(in);
+ throttledTransfer(throttlePolicy, socket, source, Okio.buffer(requestBody), chunkSize);
+ readEmptyLine(source);
}
}
@@ -507,79 +596,139 @@
throw new UnsupportedOperationException("Unexpected method: " + request);
}
- return new RecordedRequest(request, headers, chunkSizes, requestBody.numBytesReceived,
- requestBody.toByteArray(), sequenceNumber, socket);
+ return new RecordedRequest(request, headers.build(), chunkSizes, requestBody.receivedByteCount,
+ requestBody.buffer, sequenceNumber, socket);
}
- private void writeResponse(OutputStream out, MockResponse response) throws IOException {
- out.write((response.getStatus() + "\r\n").getBytes(Util.US_ASCII));
- List<String> headers = response.getHeaders();
- for (int i = 0, size = headers.size(); i < size; i++) {
- String header = headers.get(i);
- out.write((header + "\r\n").getBytes(Util.US_ASCII));
- }
- out.write(("\r\n").getBytes(Util.US_ASCII));
- out.flush();
+ private void handleWebSocketUpgrade(Socket socket, BufferedSource source, BufferedSink sink,
+ RecordedRequest request, MockResponse response) throws IOException {
+ String key = request.getHeader("Sec-WebSocket-Key");
+ String acceptKey = Util.shaBase64(key + WebSocketProtocol.ACCEPT_MAGIC);
+ response.setHeader("Sec-WebSocket-Accept", acceptKey);
- InputStream in = response.getBodyStream();
- if (in == null) return;
- throttledTransfer(response, in, out, Long.MAX_VALUE);
+ writeHttpResponse(socket, sink, response);
+
+ final WebSocketListener listener = response.getWebSocketListener();
+ final CountDownLatch connectionClose = new CountDownLatch(1);
+ final RealWebSocket webSocket =
+ new RealWebSocket(false, source, sink, new SecureRandom(), listener,
+ request.getPath()) {
+ @Override protected void closeConnection() throws IOException {
+ connectionClose.countDown();
+ }
+ };
+
+ // Adapt the request and response into our Request and Response domain model.
+ final Request fancyRequest = new Request.Builder()
+ .get().url(request.getPath())
+ .headers(request.getHeaders())
+ .build();
+ final Response fancyResponse = new Response.Builder()
+ .code(Integer.parseInt(response.getStatus().split(" ")[1]))
+ .message(response.getStatus().split(" ", 3)[2])
+ .headers(response.getHeaders())
+ .request(fancyRequest)
+ .protocol(Protocol.HTTP_1_1)
+ .build();
+
+ // The callback might act synchronously. Give it its own thread.
+ new Thread(new Runnable() {
+ @Override public void run() {
+ try {
+ listener.onOpen(webSocket, fancyRequest, fancyResponse);
+ } catch (IOException e) {
+ // TODO try to write close frame?
+ connectionClose.countDown();
+ }
+ }
+ }, "MockWebServer WebSocket Writer " + request.getPath()).start();
+
+ // Use this thread to continuously read messages.
+ while (webSocket.readMessage()) {
+ }
+
+ // Even if messages are no longer being read we need to wait for the connection close signal.
+ try {
+ connectionClose.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ Util.closeQuietly(sink);
+ Util.closeQuietly(source);
+ }
+
+ private void writeHttpResponse(Socket socket, BufferedSink sink, MockResponse response)
+ throws IOException {
+ sink.writeUtf8(response.getStatus());
+ sink.writeUtf8("\r\n");
+
+ Headers headers = response.getHeaders();
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ sink.writeUtf8(headers.name(i));
+ sink.writeUtf8(": ");
+ sink.writeUtf8(headers.value(i));
+ sink.writeUtf8("\r\n");
+ }
+ sink.writeUtf8("\r\n");
+ sink.flush();
+
+ Buffer body = response.getBody();
+ if (body == null) return;
+ sleepIfDelayed(response);
+ throttledTransfer(response, socket, body, sink, Long.MAX_VALUE);
+ }
+
+ private void sleepIfDelayed(MockResponse response) {
+ long delayMs = response.getBodyDelay(TimeUnit.MILLISECONDS);
+ if (delayMs != 0) {
+ try {
+ Thread.sleep(delayMs);
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ }
}
/**
- * Transfer bytes from {@code in} to {@code out} until either {@code length}
- * bytes have been transferred or {@code in} is exhausted. The transfer is
+ * Transfer bytes from {@code source} to {@code sink} until either {@code byteCount}
+ * bytes have been transferred or {@code source} is exhausted. The transfer is
* throttled according to {@code throttlePolicy}.
*/
- private void throttledTransfer(MockResponse throttlePolicy, InputStream in, OutputStream out,
- long limit) throws IOException {
- byte[] buffer = new byte[1024];
- int bytesPerPeriod = throttlePolicy.getThrottleBytesPerPeriod();
- long delayMs = throttlePolicy.getThrottleUnit().toMillis(throttlePolicy.getThrottlePeriod());
+ private void throttledTransfer(MockResponse throttlePolicy, Socket socket, BufferedSource source,
+ BufferedSink sink, long byteCount) throws IOException {
+ if (byteCount == 0) return;
- while (true) {
+ Buffer buffer = new Buffer();
+ long bytesPerPeriod = throttlePolicy.getThrottleBytesPerPeriod();
+ long periodDelayMs = throttlePolicy.getThrottlePeriod(TimeUnit.MILLISECONDS);
+
+ while (!socket.isClosed()) {
for (int b = 0; b < bytesPerPeriod; ) {
- int toRead = (int) Math.min(Math.min(buffer.length, limit), bytesPerPeriod - b);
- int read = in.read(buffer, 0, toRead);
+ long toRead = Math.min(Math.min(2048, byteCount), bytesPerPeriod - b);
+ long read = source.read(buffer, toRead);
if (read == -1) return;
- out.write(buffer, 0, read);
- out.flush();
+ sink.write(buffer, read);
+ sink.flush();
b += read;
- limit -= read;
+ byteCount -= read;
- if (limit == 0) return;
+ if (byteCount == 0) return;
}
- try {
- if (delayMs != 0) Thread.sleep(delayMs);
- } catch (InterruptedException e) {
- throw new AssertionError();
+ if (periodDelayMs != 0) {
+ try {
+ Thread.sleep(periodDelayMs);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
}
}
}
- /**
- * Returns the text from {@code in} until the next "\r\n", or null if {@code
- * in} is exhausted.
- */
- private String readAsciiUntilCrlf(InputStream in) throws IOException {
- StringBuilder builder = new StringBuilder();
- while (true) {
- int c = in.read();
- if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') {
- builder.deleteCharAt(builder.length() - 1);
- return builder.toString();
- } else if (c == -1) {
- return builder.toString();
- } else {
- builder.append((char) c);
- }
- }
- }
-
- private void readEmptyLine(InputStream in) throws IOException {
- String line = readAsciiUntilCrlf(in);
+ private void readEmptyLine(BufferedSource source) throws IOException {
+ String line = source.readUtf8LineStrict();
if (line.length() != 0) throw new IllegalStateException("Expected empty but was: " + line);
}
@@ -594,20 +743,41 @@
this.dispatcher = dispatcher;
}
- /** An output stream that drops data after bodyLimit bytes. */
- private class TruncatingOutputStream extends ByteArrayOutputStream {
- private long numBytesReceived = 0;
+ @Override public String toString() {
+ return "MockWebServer[" + port + "]";
+ }
- @Override public void write(byte[] buffer, int offset, int len) {
- numBytesReceived += len;
- super.write(buffer, offset, Math.min(len, bodyLimit - count));
+ /** A buffer wrapper that drops data after {@code bodyLimit} bytes. */
+ private static class TruncatingBuffer implements Sink {
+ private final Buffer buffer = new Buffer();
+ private long remainingByteCount;
+ private long receivedByteCount;
+
+ TruncatingBuffer(long bodyLimit) {
+ remainingByteCount = bodyLimit;
}
- @Override public void write(int oneByte) {
- numBytesReceived++;
- if (count < bodyLimit) {
- super.write(oneByte);
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ long toRead = Math.min(remainingByteCount, byteCount);
+ if (toRead > 0) {
+ source.read(buffer, toRead);
}
+ long toSkip = byteCount - toRead;
+ if (toSkip > 0) {
+ source.skip(toSkip);
+ }
+ remainingByteCount -= toRead;
+ receivedByteCount += byteCount;
+ }
+
+ @Override public void flush() throws IOException {
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
}
}
@@ -633,14 +803,14 @@
}
writeResponse(stream, response);
if (logger.isLoggable(Level.INFO)) {
- logger.info("Received request: " + request + " and responded: " + response
- + " protocol is " + protocol.name.utf8());
+ logger.info(MockWebServer.this + " received request: " + request
+ + " and responded: " + response + " protocol is " + protocol.toString());
}
}
private RecordedRequest readRequest(SpdyStream stream) throws IOException {
List<Header> spdyHeaders = stream.getRequestHeaders();
- List<String> httpHeaders = new ArrayList<String>();
+ Headers.Builder httpHeaders = new Headers.Builder();
String method = "<:method omitted>";
String path = "<:path omitted>";
String version = protocol == Protocol.SPDY_3 ? "<:version omitted>" : "HTTP/1.1";
@@ -654,29 +824,25 @@
} else if (name.equals(Header.VERSION)) {
version = value;
} else {
- httpHeaders.add(name.utf8() + ": " + value);
+ httpHeaders.add(name.utf8(), value);
}
}
- InputStream bodyIn = Okio.buffer(stream.getSource()).inputStream();
- ByteArrayOutputStream bodyOut = new ByteArrayOutputStream();
- byte[] buffer = new byte[8192];
- int count;
- while ((count = bodyIn.read(buffer)) != -1) {
- bodyOut.write(buffer, 0, count);
- }
- bodyIn.close();
+ Buffer body = new Buffer();
+ body.writeAll(stream.getSource());
+ body.close();
+
String requestLine = method + ' ' + path + ' ' + version;
List<Integer> chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY.
- return new RecordedRequest(requestLine, httpHeaders, chunkSizes, bodyOut.size(),
- bodyOut.toByteArray(), sequenceNumber.getAndIncrement(), socket);
+ return new RecordedRequest(requestLine, httpHeaders.build(), chunkSizes, body.size(), body,
+ sequenceNumber.getAndIncrement(), socket);
}
private void writeResponse(SpdyStream stream, MockResponse response) throws IOException {
if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
return;
}
- List<Header> spdyHeaders = new ArrayList<Header>();
+ List<Header> spdyHeaders = new ArrayList<>();
String[] statusParts = response.getStatus().split(" ", 2);
if (statusParts.length != 2) {
throw new AssertionError("Unexpected status: " + response.getStatus());
@@ -686,47 +852,19 @@
if (protocol == Protocol.SPDY_3) {
spdyHeaders.add(new Header(Header.VERSION, statusParts[0]));
}
- List<String> headers = response.getHeaders();
+ Headers headers = response.getHeaders();
for (int i = 0, size = headers.size(); i < size; i++) {
- String header = headers.get(i);
- String[] headerParts = header.split(":", 2);
- if (headerParts.length != 2) {
- throw new AssertionError("Unexpected header: " + header);
- }
- spdyHeaders.add(new Header(headerParts[0], headerParts[1]));
+ spdyHeaders.add(new Header(headers.name(i), headers.value(i)));
}
- OkBuffer body = new OkBuffer();
- if (response.getBody() != null) {
- body.write(response.getBody());
- }
- boolean closeStreamAfterHeaders = body.size() > 0 || !response.getPushPromises().isEmpty();
+
+ Buffer body = response.getBody();
+ boolean closeStreamAfterHeaders = body != null || !response.getPushPromises().isEmpty();
stream.reply(spdyHeaders, closeStreamAfterHeaders);
pushPromises(stream, response.getPushPromises());
- if (body.size() > 0) {
- if (response.getBodyDelayTimeMs() != 0) {
- try {
- Thread.sleep(response.getBodyDelayTimeMs());
- } catch (InterruptedException e) {
- throw new AssertionError(e);
- }
- }
+ if (body != null) {
BufferedSink sink = Okio.buffer(stream.getSink());
- if (response.getThrottleBytesPerPeriod() == Integer.MAX_VALUE) {
- sink.write(body, body.size());
- sink.flush();
- } else {
- while (body.size() > 0) {
- long toWrite = Math.min(body.size(), response.getThrottleBytesPerPeriod());
- sink.write(body, toWrite);
- sink.flush();
- try {
- long delayMs = response.getThrottleUnit().toMillis(response.getThrottlePeriod());
- if (delayMs != 0) Thread.sleep(delayMs);
- } catch (InterruptedException e) {
- throw new AssertionError();
- }
- }
- }
+ sleepIfDelayed(response);
+ throttledTransfer(response, socket, body, sink, bodyLimit);
sink.close();
} else if (closeStreamAfterHeaders) {
stream.close(ErrorCode.NO_ERROR);
@@ -735,27 +873,23 @@
private void pushPromises(SpdyStream stream, List<PushPromise> promises) throws IOException {
for (PushPromise pushPromise : promises) {
- List<Header> pushedHeaders = new ArrayList<Header>();
+ List<Header> pushedHeaders = new ArrayList<>();
pushedHeaders.add(new Header(stream.getConnection().getProtocol() == Protocol.SPDY_3
? Header.TARGET_HOST
: Header.TARGET_AUTHORITY, getUrl(pushPromise.getPath()).getHost()));
pushedHeaders.add(new Header(Header.TARGET_METHOD, pushPromise.getMethod()));
pushedHeaders.add(new Header(Header.TARGET_PATH, pushPromise.getPath()));
- for (int i = 0, size = pushPromise.getHeaders().size(); i < size; i++) {
- String header = pushPromise.getHeaders().get(i);
- String[] headerParts = header.split(":", 2);
- if (headerParts.length != 2) {
- throw new AssertionError("Unexpected header: " + header);
- }
- pushedHeaders.add(new Header(headerParts[0], headerParts[1].trim()));
+ Headers pushPromiseHeaders = pushPromise.getHeaders();
+ for (int i = 0, size = pushPromiseHeaders.size(); i < size; i++) {
+ pushedHeaders.add(new Header(pushPromiseHeaders.name(i), pushPromiseHeaders.value(i)));
}
String requestLine = pushPromise.getMethod() + ' ' + pushPromise.getPath() + " HTTP/1.1";
List<Integer> chunkSizes = Collections.emptyList(); // No chunked encoding for SPDY.
requestQueue.add(new RecordedRequest(requestLine, pushPromise.getHeaders(), chunkSizes, 0,
- Util.EMPTY_BYTE_ARRAY, sequenceNumber.getAndIncrement(), socket));
- byte[] pushedBody = pushPromise.getResponse().getBody();
+ new Buffer(), sequenceNumber.getAndIncrement(), socket));
+ boolean hasBody = pushPromise.getResponse().getBody() != null;
SpdyStream pushedStream =
- stream.getConnection().pushStream(stream.getId(), pushedHeaders, pushedBody.length > 0);
+ stream.getConnection().pushStream(stream.getId(), pushedHeaders, hasBody);
writeResponse(pushedStream, pushPromise.getResponse());
}
}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java
index d9dd019..649b4ee 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/PushPromise.java
@@ -15,16 +15,16 @@
*/
package com.squareup.okhttp.mockwebserver;
-import java.util.List;
+import com.squareup.okhttp.Headers;
/** An HTTP request initiated by the server. */
public final class PushPromise {
private final String method;
private final String path;
- private final List<String> headers;
+ private final Headers headers;
private final MockResponse response;
- public PushPromise(String method, String path, List<String> headers, MockResponse response) {
+ public PushPromise(String method, String path, Headers headers, MockResponse response) {
this.method = method;
this.path = path;
this.headers = headers;
@@ -39,7 +39,7 @@
return path;
}
- public List<String> getHeaders() {
+ public Headers getHeaders() {
return headers;
}
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 7c1ddd1..c9c206c 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
@@ -24,8 +24,7 @@
* by calling {@link #enqueueResponse(MockResponse)}.
*/
public class QueueDispatcher extends Dispatcher {
- protected final BlockingQueue<MockResponse> responseQueue
- = new LinkedBlockingQueue<MockResponse>();
+ protected final BlockingQueue<MockResponse> responseQueue = new LinkedBlockingQueue<>();
private MockResponse failFastResponse;
@Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
index 58b5d10..99d4d27 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/RecordedRequest.java
@@ -16,34 +16,35 @@
package com.squareup.okhttp.mockwebserver;
-import java.io.UnsupportedEncodingException;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.TlsVersion;
import java.net.Socket;
-import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLSocket;
+import okio.Buffer;
/** An HTTP request that came into the mock web server. */
public final class RecordedRequest {
private final String requestLine;
private final String method;
private final String path;
- private final List<String> headers;
+ private final Headers headers;
private final List<Integer> chunkSizes;
private final long bodySize;
- private final byte[] body;
+ private final Buffer body;
private final int sequenceNumber;
- private final String sslProtocol;
+ private final TlsVersion tlsVersion;
- public RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes,
- long bodySize, byte[] body, int sequenceNumber, Socket socket) {
+ public RecordedRequest(String requestLine, Headers headers, List<Integer> chunkSizes,
+ long bodySize, Buffer body, int sequenceNumber, Socket socket) {
this.requestLine = requestLine;
this.headers = headers;
this.chunkSizes = chunkSizes;
this.bodySize = bodySize;
this.body = body;
this.sequenceNumber = sequenceNumber;
- this.sslProtocol = socket instanceof SSLSocket
- ? ((SSLSocket) socket).getSession().getProtocol()
+ this.tlsVersion = socket instanceof SSLSocket
+ ? TlsVersion.forJavaName(((SSLSocket) socket).getSession().getProtocol())
: null;
if (requestLine != null) {
@@ -70,36 +71,14 @@
}
/** Returns all headers. */
- public List<String> getHeaders() {
+ public Headers getHeaders() {
return headers;
}
- /**
- * Returns the first header named {@code name}, or null if no such header
- * exists.
- */
+ /** Returns the first header named {@code name}, or null if no such header exists. */
public String getHeader(String name) {
- name += ":";
- for (int i = 0, size = headers.size(); i < size; i++) {
- String header = headers.get(i);
- if (name.regionMatches(true, 0, header, 0, name.length())) {
- return header.substring(name.length()).trim();
- }
- }
- return null;
- }
-
- /** Returns the headers named {@code name}. */
- public List<String> getHeaders(String name) {
- List<String> result = new ArrayList<String>();
- name += ":";
- for (int i = 0, size = headers.size(); i < size; i++) {
- String header = headers.get(i);
- if (name.regionMatches(true, 0, header, 0, name.length())) {
- result.add(header.substring(name.length()).trim());
- }
- }
- return result;
+ List<String> values = headers.values(name);
+ return values.isEmpty() ? null : values.get(0);
}
/**
@@ -119,17 +98,13 @@
}
/** Returns the body of this POST request. This may be truncated. */
- public byte[] getBody() {
+ public Buffer getBody() {
return body;
}
- /** Returns the body of this POST request decoded as a UTF-8 string. */
+ /** @deprecated Use {@link #getBody() getBody().readUtf8()}. */
public String getUtf8Body() {
- try {
- return new String(body, "UTF-8");
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError();
- }
+ return getBody().readUtf8();
}
/**
@@ -141,12 +116,9 @@
return sequenceNumber;
}
- /**
- * Returns the connection's SSL protocol like {@code TLSv1}, {@code SSLv3},
- * {@code NONE} or null if the connection doesn't use SSL.
- */
- public String getSslProtocol() {
- return sslProtocol;
+ /** Returns the connection's TLS version or null if the connection doesn't use SSL. */
+ public TlsVersion getTlsVersion() {
+ return tlsVersion;
}
@Override public String toString() {
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 76701c4..e2d5f28 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -44,6 +44,12 @@
*/
DISCONNECT_AT_START,
+ /**
+ * Close connection after reading the request but before writing the response.
+ * Use this to simulate late connection pool failures.
+ */
+ DISCONNECT_AFTER_REQUEST,
+
/** Don't trust the client during the SSL handshake. */
FAIL_HANDSHAKE,
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRule.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRule.java
new file mode 100644
index 0000000..01df8e2
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRule.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2014 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.mockwebserver.rule;
+
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import java.io.IOException;
+import java.net.URL;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import org.junit.rules.ExternalResource;
+
+/**
+ * Allows you to use {@link MockWebServer} as a JUnit test rule.
+ *
+ * <p>This rule starts {@link MockWebServer} on an available port before your test runs, and shuts
+ * it down after it completes.
+ */
+public class MockWebServerRule extends ExternalResource {
+ private static final Logger logger = Logger.getLogger(MockWebServerRule.class.getName());
+
+ private final MockWebServer server = new MockWebServer();
+ private boolean started;
+
+ @Override protected void before() {
+ if (started) return;
+ started = true;
+ try {
+ server.start();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override protected void after() {
+ try {
+ server.shutdown();
+ } catch (IOException e) {
+ logger.log(Level.WARNING, "MockWebServer shutdown failed", e);
+ }
+ }
+
+ public String getHostName() {
+ if (!started) before();
+ return server.getHostName();
+ }
+
+ public int getPort() {
+ if (!started) before();
+ return server.getPort();
+ }
+
+ public int getRequestCount() {
+ return server.getRequestCount();
+ }
+
+ public void enqueue(MockResponse response) {
+ server.enqueue(response);
+ }
+
+ public RecordedRequest takeRequest() throws InterruptedException {
+ return server.takeRequest();
+ }
+
+ public URL getUrl(String path) {
+ return server.getUrl(path);
+ }
+
+ /** For any other functionality, use the {@linkplain MockWebServer} directly. */
+ public MockWebServer get() {
+ return server;
+ }
+}
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
index 7b7e112..1c8c820 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/CustomDispatcherTest.java
@@ -22,78 +22,78 @@
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
-import junit.framework.TestCase;
+import org.junit.After;
+import org.junit.Test;
-public class CustomDispatcherTest extends TestCase {
+import static org.junit.Assert.assertEquals;
- private MockWebServer mockWebServer = new MockWebServer();
+public class CustomDispatcherTest {
+ private MockWebServer mockWebServer = new MockWebServer();
- @Override
- public void tearDown() throws Exception {
- mockWebServer.shutdown();
- }
+ @After public void tearDown() throws Exception {
+ mockWebServer.shutdown();
+ }
- public void testSimpleDispatch() throws Exception {
- mockWebServer.play();
- final List<RecordedRequest> requestsMade = new ArrayList<RecordedRequest>();
- final Dispatcher dispatcher = new Dispatcher() {
- @Override
- public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
- requestsMade.add(request);
- return new MockResponse();
- }
- };
- assertEquals(0, requestsMade.size());
- mockWebServer.setDispatcher(dispatcher);
- final URL url = mockWebServer.getUrl("/");
- final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
- conn.getResponseCode(); // Force the connection to hit the "server".
- // Make sure our dispatcher got the request.
- assertEquals(1, requestsMade.size());
- }
+ @Test public void simpleDispatch() throws Exception {
+ mockWebServer.start();
+ final List<RecordedRequest> requestsMade = new ArrayList<>();
+ final Dispatcher dispatcher = new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ requestsMade.add(request);
+ return new MockResponse();
+ }
+ };
+ assertEquals(0, requestsMade.size());
+ mockWebServer.setDispatcher(dispatcher);
+ final URL url = mockWebServer.getUrl("/");
+ final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.getResponseCode(); // Force the connection to hit the "server".
+ // Make sure our dispatcher got the request.
+ assertEquals(1, requestsMade.size());
+ }
- public void testOutOfOrderResponses() throws Exception {
- AtomicInteger firstResponseCode = new AtomicInteger();
- AtomicInteger secondResponseCode = new AtomicInteger();
- mockWebServer.play();
- final String secondRequest = "/bar";
- final String firstRequest = "/foo";
- final CountDownLatch latch = new CountDownLatch(1);
- final Dispatcher dispatcher = new Dispatcher() {
- @Override
- public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
- if (request.getPath().equals(firstRequest)) {
- latch.await();
- }
- return new MockResponse();
- }
- };
- mockWebServer.setDispatcher(dispatcher);
- final Thread startsFirst = buildRequestThread(firstRequest, firstResponseCode);
- startsFirst.start();
- final Thread endsFirst = buildRequestThread(secondRequest, secondResponseCode);
- endsFirst.start();
- endsFirst.join();
- assertEquals(0, firstResponseCode.get()); // First response is still waiting.
- assertEquals(200, secondResponseCode.get()); // Second response is done.
- latch.countDown();
- startsFirst.join();
- assertEquals(200, firstResponseCode.get()); // And now it's done!
- assertEquals(200, secondResponseCode.get()); // (Still done).
- }
+ @Test public void outOfOrderResponses() throws Exception {
+ AtomicInteger firstResponseCode = new AtomicInteger();
+ AtomicInteger secondResponseCode = new AtomicInteger();
+ mockWebServer.start();
+ final String secondRequest = "/bar";
+ final String firstRequest = "/foo";
+ final CountDownLatch latch = new CountDownLatch(1);
+ final Dispatcher dispatcher = new Dispatcher() {
+ @Override
+ public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
+ if (request.getPath().equals(firstRequest)) {
+ latch.await();
+ }
+ return new MockResponse();
+ }
+ };
+ mockWebServer.setDispatcher(dispatcher);
+ final Thread startsFirst = buildRequestThread(firstRequest, firstResponseCode);
+ startsFirst.start();
+ final Thread endsFirst = buildRequestThread(secondRequest, secondResponseCode);
+ endsFirst.start();
+ endsFirst.join();
+ assertEquals(0, firstResponseCode.get()); // First response is still waiting.
+ assertEquals(200, secondResponseCode.get()); // Second response is done.
+ latch.countDown();
+ startsFirst.join();
+ assertEquals(200, firstResponseCode.get()); // And now it's done!
+ assertEquals(200, secondResponseCode.get()); // (Still done).
+ }
- private Thread buildRequestThread(final String path, final AtomicInteger responseCode) {
- return new Thread(new Runnable() {
- @Override public void run() {
- final URL url = mockWebServer.getUrl(path);
- final HttpURLConnection conn;
- try {
- conn = (HttpURLConnection) url.openConnection();
- responseCode.set(conn.getResponseCode()); // Force the connection to hit the "server".
- } catch (IOException e) {
- }
- }
- });
- }
-
+ private Thread buildRequestThread(final String path, final AtomicInteger responseCode) {
+ return new Thread(new Runnable() {
+ @Override public void run() {
+ final URL url = mockWebServer.getUrl(path);
+ final HttpURLConnection conn;
+ try {
+ conn = (HttpURLConnection) url.openConnection();
+ responseCode.set(conn.getResponseCode()); // Force the connection to hit the "server".
+ } catch (IOException e) {
+ }
+ }
+ });
+ }
}
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 2b1651f..388dbf6 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package com.squareup.okhttp.mockwebserver;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -25,317 +25,243 @@
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
+import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import junit.framework.TestCase;
+import org.junit.Rule;
+import org.junit.Test;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
-public final class MockWebServerTest extends TestCase {
+public final class MockWebServerTest {
+ @Rule public final MockWebServerRule server = new MockWebServerRule();
- private MockWebServer server = new MockWebServer();
+ @Test public void defaultMockResponse() {
+ MockResponse response = new MockResponse();
+ assertEquals(Arrays.asList("Content-Length: 0"), headersToList(response));
+ assertEquals("HTTP/1.1 200 OK", response.getStatus());
+ }
- @Override protected void tearDown() throws Exception {
- server.shutdown();
- super.tearDown();
- }
+ @Test public void setBodyAdjustsHeaders() throws IOException {
+ MockResponse response = new MockResponse().setBody("ABC");
+ assertEquals(Arrays.asList("Content-Length: 3"), headersToList(response));
+ assertEquals("ABC", response.getBody().readUtf8());
+ assertEquals("HTTP/1.1 200 OK", response.getStatus());
+ }
- public void testRecordedRequestAccessors() {
- List<String> headers = Arrays.asList(
- "User-Agent: okhttp",
- "Cookie: s=square",
- "Cookie: a=android",
- "X-Whitespace: left",
- "X-Whitespace:right ",
- "X-Whitespace: both "
- );
- List<Integer> chunkSizes = Collections.emptyList();
- byte[] body = {'A', 'B', 'C'};
- String requestLine = "GET / HTTP/1.1";
- RecordedRequest request = new RecordedRequest(
- requestLine, headers, chunkSizes, body.length, body, 0, null);
- assertEquals("s=square", request.getHeader("cookie"));
- assertEquals(Arrays.asList("s=square", "a=android"), request.getHeaders("cookie"));
- assertEquals("left", request.getHeader("x-whitespace"));
- assertEquals(Arrays.asList("left", "right", "both"), request.getHeaders("x-whitespace"));
- assertEquals("ABC", request.getUtf8Body());
- }
+ @Test public void mockResponseAddHeader() {
+ MockResponse response = new MockResponse()
+ .clearHeaders()
+ .addHeader("Cookie: s=square")
+ .addHeader("Cookie", "a=android");
+ assertEquals(Arrays.asList("Cookie: s=square", "Cookie: a=android"), headersToList(response));
+ }
- public void testDefaultMockResponse() {
- MockResponse response = new MockResponse();
- assertEquals(Arrays.asList("Content-Length: 0"), response.getHeaders());
- assertEquals("HTTP/1.1 200 OK", response.getStatus());
- }
+ @Test public void mockResponseSetHeader() {
+ MockResponse response = new MockResponse()
+ .clearHeaders()
+ .addHeader("Cookie: s=square")
+ .addHeader("Cookie: a=android")
+ .addHeader("Cookies: delicious");
+ response.setHeader("cookie", "r=robot");
+ assertEquals(Arrays.asList("Cookies: delicious", "cookie: r=robot"), headersToList(response));
+ }
- public void testSetBodyAdjustsHeaders() throws IOException {
- MockResponse response = new MockResponse().setBody("ABC");
- assertEquals(Arrays.asList("Content-Length: 3"), response.getHeaders());
- InputStream in = response.getBodyStream();
- assertEquals('A', in.read());
- assertEquals('B', in.read());
- assertEquals('C', in.read());
- assertEquals(-1, in.read());
- assertEquals("HTTP/1.1 200 OK", response.getStatus());
- }
+ @Test public void regularResponse() throws Exception {
+ server.enqueue(new MockResponse().setBody("hello world"));
- public void testMockResponseAddHeader() {
- MockResponse response = new MockResponse()
- .clearHeaders()
- .addHeader("Cookie: s=square")
- .addHeader("Cookie", "a=android");
- assertEquals(Arrays.asList("Cookie: s=square", "Cookie: a=android"),
- response.getHeaders());
- }
+ URL url = server.getUrl("/");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestProperty("Accept-Language", "en-US");
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
+ assertEquals("hello world", reader.readLine());
- public void testMockResponseSetHeader() {
- MockResponse response = new MockResponse()
- .clearHeaders()
- .addHeader("Cookie: s=square")
- .addHeader("Cookie: a=android")
- .addHeader("Cookies: delicious");
- response.setHeader("cookie", "r=robot");
- assertEquals(Arrays.asList("Cookies: delicious", "cookie: r=robot"),
- response.getHeaders());
- }
+ RecordedRequest request = server.takeRequest();
+ assertEquals("GET / HTTP/1.1", request.getRequestLine());
+ assertEquals("en-US", request.getHeader("Accept-Language"));
+ }
- /**
- * Clients who adhere to <a
- * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3">100
- * Status</a> expect the server to send an interim response with status code
- * 100 before they send their payload.
- *
- * <h4>Note</h4>
- *
- * JRE 6 only passes this test if
- * {@code -Dsun.net.http.allowRestrictedHeaders=true} is set.
- */
- public void testExpect100ContinueWithBody() throws Exception {
- server.enqueue(new MockResponse());
- server.play();
+ @Test public void redirect() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ .addHeader("Location: " + server.getUrl("/new-path"))
+ .setBody("This page has moved!"));
+ server.enqueue(new MockResponse().setBody("This is the new location!"));
- URL url = server.getUrl("/");
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setRequestMethod("PUT");
- connection.setAllowUserInteraction(false);
- connection.setRequestProperty("Expect", "100-continue");
- connection.setDoOutput(true);
- connection.getOutputStream().write("hello".getBytes());
- assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals("This is the new location!", reader.readLine());
- assertEquals(server.getRequestCount(), 1);
- RecordedRequest request = server.takeRequest();
- assertEquals(request.getRequestLine(), "PUT / HTTP/1.1");
- assertEquals("5", request.getHeader("Content-Length"));
- assertEquals(5, request.getBodySize());
- assertEquals("hello", new String(request.getBody()));
- // below fails on JRE 6 unless -Dsun.net.http.allowRestrictedHeaders=true is set
- assertEquals("100-continue", request.getHeader("Expect"));
- }
+ RecordedRequest first = server.takeRequest();
+ assertEquals("GET / HTTP/1.1", first.getRequestLine());
+ RecordedRequest redirect = server.takeRequest();
+ assertEquals("GET /new-path HTTP/1.1", redirect.getRequestLine());
+ }
- public void testExpect100ContinueWithNoBody() throws Exception {
- server.enqueue(new MockResponse());
- server.play();
-
- URL url = server.getUrl("/");
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setRequestMethod("PUT");
- connection.setAllowUserInteraction(false);
- connection.setRequestProperty("Expect", "100-continue");
- connection.setRequestProperty("Content-Length", "0");
- connection.setDoOutput(true);
- connection.setFixedLengthStreamingMode(0);
- assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
-
- assertEquals(server.getRequestCount(), 1);
- RecordedRequest request = server.takeRequest();
- assertEquals(request.getRequestLine(), "PUT / HTTP/1.1");
- assertEquals("0", request.getHeader("Content-Length"));
- assertEquals(0, request.getBodySize());
- // below fails on JRE 6 unless -Dsun.net.http.allowRestrictedHeaders=true is set
- assertEquals("100-continue", request.getHeader("Expect"));
- }
-
- public void testRegularResponse() throws Exception {
- server.enqueue(new MockResponse().setBody("hello world"));
- server.play();
-
- URL url = server.getUrl("/");
- HttpURLConnection connection = (HttpURLConnection) url.openConnection();
- connection.setRequestProperty("Accept-Language", "en-US");
- InputStream in = connection.getInputStream();
- BufferedReader reader = new BufferedReader(new InputStreamReader(in));
- assertEquals(HttpURLConnection.HTTP_OK, connection.getResponseCode());
- assertEquals("hello world", reader.readLine());
-
- RecordedRequest request = server.takeRequest();
- assertEquals("GET / HTTP/1.1", request.getRequestLine());
- assertTrue(request.getHeaders().contains("Accept-Language: en-US"));
- }
-
- public void testRedirect() throws Exception {
- server.play();
- server.enqueue(new MockResponse()
- .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
- .addHeader("Location: " + server.getUrl("/new-path"))
- .setBody("This page has moved!"));
- server.enqueue(new MockResponse().setBody("This is the new location!"));
-
- URLConnection connection = server.getUrl("/").openConnection();
- InputStream in = connection.getInputStream();
- BufferedReader reader = new BufferedReader(new InputStreamReader(in));
- assertEquals("This is the new location!", reader.readLine());
-
- RecordedRequest first = server.takeRequest();
- assertEquals("GET / HTTP/1.1", first.getRequestLine());
- RecordedRequest redirect = server.takeRequest();
- assertEquals("GET /new-path HTTP/1.1", redirect.getRequestLine());
- }
-
- /**
- * Test that MockWebServer blocks for a call to enqueue() if a request
- * is made before a mock response is ready.
- */
- public void testDispatchBlocksWaitingForEnqueue() throws Exception {
- server.play();
-
- new Thread() {
- @Override public void run() {
- try {
- Thread.sleep(1000);
- } catch (InterruptedException ignored) {
- }
- server.enqueue(new MockResponse().setBody("enqueued in the background"));
- }
- }.start();
-
- URLConnection connection = server.getUrl("/").openConnection();
- InputStream in = connection.getInputStream();
- BufferedReader reader = new BufferedReader(new InputStreamReader(in));
- assertEquals("enqueued in the background", reader.readLine());
- }
-
- public void testNonHexadecimalChunkSize() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n")
- .clearHeaders()
- .addHeader("Transfer-encoding: chunked"));
- server.play();
-
- URLConnection connection = server.getUrl("/").openConnection();
- InputStream in = connection.getInputStream();
+ /**
+ * Test that MockWebServer blocks for a call to enqueue() if a request
+ * is made before a mock response is ready.
+ */
+ @Test public void dispatchBlocksWaitingForEnqueue() throws Exception {
+ new Thread() {
+ @Override public void run() {
try {
- in.read();
- fail();
- } catch (IOException expected) {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignored) {
}
+ server.enqueue(new MockResponse().setBody("enqueued in the background"));
+ }
+ }.start();
+
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals("enqueued in the background", reader.readLine());
+ }
+
+ @Test public void nonHexadecimalChunkSize() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("G\r\nxxxxxxxxxxxxxxxx\r\n0\r\n\r\n")
+ .clearHeaders()
+ .addHeader("Transfer-encoding: chunked"));
+
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ try {
+ in.read();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ @Test public void responseTimeout() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("ABC")
+ .clearHeaders()
+ .addHeader("Content-Length: 4"));
+ server.enqueue(new MockResponse().setBody("DEF"));
+
+ URLConnection urlConnection = server.getUrl("/").openConnection();
+ urlConnection.setReadTimeout(1000);
+ InputStream in = urlConnection.getInputStream();
+ assertEquals('A', in.read());
+ assertEquals('B', in.read());
+ assertEquals('C', in.read());
+ try {
+ in.read(); // if Content-Length was accurate, this would return -1 immediately
+ fail();
+ } catch (SocketTimeoutException expected) {
}
- public void testResponseTimeout() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("ABC")
- .clearHeaders()
- .addHeader("Content-Length: 4"));
- server.enqueue(new MockResponse()
- .setBody("DEF"));
- server.play();
+ URLConnection urlConnection2 = server.getUrl("/").openConnection();
+ InputStream in2 = urlConnection2.getInputStream();
+ assertEquals('D', in2.read());
+ assertEquals('E', in2.read());
+ assertEquals('F', in2.read());
+ assertEquals(-1, in2.read());
- URLConnection urlConnection = server.getUrl("/").openConnection();
- urlConnection.setReadTimeout(1000);
- InputStream in = urlConnection.getInputStream();
- assertEquals('A', in.read());
- assertEquals('B', in.read());
- assertEquals('C', in.read());
- try {
- in.read(); // if Content-Length was accurate, this would return -1 immediately
- fail();
- } catch (SocketTimeoutException expected) {
- }
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
- URLConnection urlConnection2 = server.getUrl("/").openConnection();
- InputStream in2 = urlConnection2.getInputStream();
- assertEquals('D', in2.read());
- assertEquals('E', in2.read());
- assertEquals('F', in2.read());
- assertEquals(-1, in2.read());
-
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals(0, server.takeRequest().getSequenceNumber());
+ @Test public void disconnectAtStart() throws Exception {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse()); // The jdk's HttpUrlConnection is a bastard.
+ server.enqueue(new MockResponse());
+ try {
+ server.getUrl("/a").openConnection().getInputStream();
+ } catch (IOException expected) {
}
+ server.getUrl("/b").openConnection().getInputStream(); // Should succeed.
+ }
- public void testDisconnectAtStart() throws Exception {
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
- server.enqueue(new MockResponse()); // The jdk's HttpUrlConnection is a bastard.
- server.enqueue(new MockResponse());
- server.play();
- try {
- server.getUrl("/a").openConnection().getInputStream();
- } catch (IOException e) {
- // Expected.
- }
- server.getUrl("/b").openConnection().getInputStream(); // Should succeed.
+ /**
+ * Throttle the request body by sleeping 500ms after every 3 bytes. With a
+ * 6-byte request, this should yield one sleep for a total delay of 500ms.
+ */
+ @Test public void throttleRequest() throws Exception {
+ server.enqueue(new MockResponse()
+ .throttleBody(3, 500, TimeUnit.MILLISECONDS));
+
+ long startNanos = System.nanoTime();
+ URLConnection connection = server.getUrl("/").openConnection();
+ connection.setDoOutput(true);
+ connection.getOutputStream().write("ABCDEF".getBytes("UTF-8"));
+ InputStream in = connection.getInputStream();
+ assertEquals(-1, in.read());
+ long elapsedNanos = System.nanoTime() - startNanos;
+ long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
+
+ assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 500);
+ assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
+ }
+
+ /**
+ * Throttle the response body by sleeping 500ms after every 3 bytes. With a
+ * 6-byte response, this should yield one sleep for a total delay of 500ms.
+ */
+ @Test public void throttleResponse() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("ABCDEF")
+ .throttleBody(3, 500, TimeUnit.MILLISECONDS));
+
+ long startNanos = System.nanoTime();
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ assertEquals('A', in.read());
+ assertEquals('B', in.read());
+ assertEquals('C', in.read());
+ assertEquals('D', in.read());
+ assertEquals('E', in.read());
+ assertEquals('F', in.read());
+ assertEquals(-1, in.read());
+ long elapsedNanos = System.nanoTime() - startNanos;
+ long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
+
+ assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 500);
+ assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
+ }
+
+ /**
+ * Delay the response body by sleeping 1s.
+ */
+ @Test public void delayResponse() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("ABCDEF")
+ .setBodyDelay(1, SECONDS));
+
+ long startNanos = System.nanoTime();
+ URLConnection connection = server.getUrl("/").openConnection();
+ InputStream in = connection.getInputStream();
+ assertEquals('A', in.read());
+ assertEquals('B', in.read());
+ assertEquals('C', in.read());
+ assertEquals('D', in.read());
+ assertEquals('E', in.read());
+ assertEquals('F', in.read());
+ assertEquals(-1, in.read());
+ long elapsedNanos = System.nanoTime() - startNanos;
+ long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
+
+ assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 1000);
+ assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis <= 1100);
+ }
+
+ private List<String> headersToList(MockResponse response) {
+ Headers headers = response.getHeaders();
+ int size = headers.size();
+ List<String> headerList = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ headerList.add(headers.name(i) + ": " + headers.value(i));
}
-
- public void testStreamingResponseBody() throws Exception {
- InputStream responseBody = new ByteArrayInputStream("ABC".getBytes("UTF-8"));
- server.enqueue(new MockResponse().setBody(responseBody, 3));
- server.play();
-
- InputStream in = server.getUrl("/").openConnection().getInputStream();
- assertEquals('A', in.read());
- assertEquals('B', in.read());
- assertEquals('C', in.read());
-
- assertEquals(-1, responseBody.read()); // The body is exhausted.
- }
-
- /**
- * Throttle the request body by sleeping 500ms after every 3 bytes. With a
- * 6-byte request, this should yield one sleep for a total delay of 500ms.
- */
- public void testThrottleRequest() throws Exception {
- server.enqueue(new MockResponse()
- .throttleBody(3, 500, TimeUnit.MILLISECONDS));
- server.play();
-
- long startNanos = System.nanoTime();
- URLConnection connection = server.getUrl("/").openConnection();
- connection.setDoOutput(true);
- connection.getOutputStream().write("ABCDEF".getBytes("UTF-8"));
- InputStream in = connection.getInputStream();
- assertEquals(-1, in.read());
- long elapsedNanos = System.nanoTime() - startNanos;
- long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
-
- assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 500);
- assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
- }
-
- /**
- * Throttle the response body by sleeping 500ms after every 3 bytes. With a
- * 6-byte response, this should yield one sleep for a total delay of 500ms.
- */
- public void testThrottleResponse() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("ABCDEF")
- .throttleBody(3, 500, TimeUnit.MILLISECONDS));
- server.play();
-
- long startNanos = System.nanoTime();
- URLConnection connection = server.getUrl("/").openConnection();
- InputStream in = connection.getInputStream();
- assertEquals('A', in.read());
- assertEquals('B', in.read());
- assertEquals('C', in.read());
- assertEquals('D', in.read());
- assertEquals('E', in.read());
- assertEquals('F', in.read());
- assertEquals(-1, in.read());
- long elapsedNanos = System.nanoTime() - startNanos;
- long elapsedMillis = NANOSECONDS.toMillis(elapsedNanos);
-
- assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis >= 500);
- assertTrue(String.format("Request + Response: %sms", elapsedMillis), elapsedMillis < 1000);
- }
+ return headerList;
+ }
}
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRuleTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRuleTest.java
new file mode 100644
index 0000000..4c94efb
--- /dev/null
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/rule/MockWebServerRuleTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 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.mockwebserver.rule;
+
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.ConnectException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class MockWebServerRuleTest {
+
+ private MockWebServerRule server = new MockWebServerRule();
+
+ @After public void tearDown() {
+ server.after();
+ }
+
+ @Test public void whenRuleCreatedPortIsAvailable() throws IOException {
+ assertTrue(server.getPort() > 0);
+ }
+
+ @Test public void differentRulesGetDifferentPorts() throws IOException {
+ // ANDROID-BEGIN
+ assertTrue(server.getPort() != new MockWebServerRule().getPort());
+ // ANDROID-END
+ }
+
+ @Test public void beforePlaysServer() throws Exception {
+ server.before();
+ assertEquals(server.getPort(), server.get().getPort());
+ server.getUrl("/").openConnection().connect();
+ }
+
+ @Test public void afterStopsServer() throws Exception {
+ server.before();
+ server.after();
+
+ try {
+ server.getUrl("/").openConnection().connect();
+ fail();
+ } catch (ConnectException e) {
+ }
+ }
+
+ @Test public void typicalUsage() throws Exception {
+ server.before(); // Implicitly called when @Rule.
+
+ server.enqueue(new MockResponse().setBody("hello world"));
+
+ URL url = server.getUrl("/aaa");
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ InputStream in = connection.getInputStream();
+ BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+ assertEquals("hello world", reader.readLine());
+
+ assertEquals(1, server.getRequestCount());
+ assertEquals("GET /aaa HTTP/1.1", server.takeRequest().getRequestLine());
+
+ server.after(); // Implicitly called when @Rule.
+ }
+}
+
diff --git a/okcurl/pom.xml b/okcurl/pom.xml
index af0ba2e..0479834 100644
--- a/okcurl/pom.xml
+++ b/okcurl/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>okcurl</artifactId>
@@ -23,10 +23,6 @@
<artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
- <groupId>org.mortbay.jetty.npn</groupId>
- <artifactId>npn-boot</artifactId>
- </dependency>
- <dependency>
<groupId>io.airlift</groupId>
<artifactId>airline</artifactId>
</dependency>
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 9a45f20..e1054c9 100644
--- a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
+++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
@@ -15,16 +15,18 @@
*/
package com.squareup.okhttp.curl;
-import com.google.common.base.Function;
import com.google.common.base.Joiner;
-import com.google.common.collect.Lists;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Headers;
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.internal.http.StatusLine;
+import com.squareup.okhttp.internal.spdy.Http20Draft16;
+
import io.airlift.command.Arguments;
import io.airlift.command.Command;
import io.airlift.command.HelpOption;
@@ -34,13 +36,20 @@
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
-import java.util.Arrays;
import java.util.List;
import java.util.Properties;
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
+import okio.Okio;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -70,12 +79,7 @@
}
private static String protocols() {
- return Joiner.on(", ").join(Lists.transform(Arrays.asList(Protocol.values()),
- new Function<Protocol, String>() {
- @Override public String apply(Protocol protocol) {
- return protocol.name.utf8();
- }
- }));
+ return Joiner.on(", ").join(Protocol.values());
}
@Option(name = { "-X", "--request" }, description = "Specify request command to use")
@@ -106,6 +110,9 @@
@Option(name = { "-i", "--include" }, description = "Include protocol headers in the output")
public boolean showHeaders;
+ @Option(name = "--frames", description = "Log HTTP/2 frames to STDERR")
+ public boolean showHttp2Frames;
+
@Option(name = { "-e", "--referer" }, description = "Referer URL")
public String referer;
@@ -127,29 +134,26 @@
return;
}
+ if (showHttp2Frames) {
+ enableHttp2FrameLogging();
+ }
+
client = createClient();
Request request = createRequest();
try {
- Response response = client.execute(request);
+ Response response = client.newCall(request).execute();
if (showHeaders) {
- System.out.println(response.statusLine());
+ System.out.println(StatusLine.get(response));
Headers headers = response.headers();
- for (int i = 0, count = headers.size(); i < count; i++) {
+ for (int i = 0, size = headers.size(); i < size; i++) {
System.out.println(headers.name(i) + ": " + headers.value(i));
}
System.out.println();
}
- Response.Body body = response.body();
- byte[] buffer = new byte[1024];
- while (body.ready()) {
- int c = body.byteStream().read(buffer);
- if (c == -1) {
- return;
- }
- System.out.write(buffer, 0, c);
- }
- body.close();
+ response.body().source().readAll(Okio.sink(System.out));
+ response.body().close();
+ System.out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
@@ -159,7 +163,7 @@
private OkHttpClient createClient() {
OkHttpClient client = new OkHttpClient();
- client.setFollowProtocolRedirects(followRedirects);
+ client.setFollowSslRedirects(followRedirects);
if (connectTimeout != DEFAULT_TIMEOUT) {
client.setConnectTimeout(connectTimeout, SECONDS);
}
@@ -168,6 +172,7 @@
}
if (allowInsecure) {
client.setSslSocketFactory(createInsecureSslSocketFactory());
+ client.setHostnameVerifier(createInsecureHostnameVerifier());
}
// If we don't set this reference, there's no way to clean shutdown persistent connections.
client.setConnectionPool(ConnectionPool.getDefault());
@@ -184,7 +189,7 @@
return "GET";
}
- private Request.Body getRequestBody() {
+ private RequestBody getRequestBody() {
if (data == null) {
return null;
}
@@ -202,7 +207,7 @@
}
}
- return Request.Body.create(MediaType.parse(mimeType), bodyData);
+ return RequestBody.create(MediaType.parse(mimeType), bodyData);
}
Request createRequest() {
@@ -213,7 +218,7 @@
if (headers != null) {
for (String header : headers) {
- String[] parts = header.split(":", -1);
+ String[] parts = header.split(":", 2);
request.header(parts[0], parts[1]);
}
}
@@ -251,4 +256,25 @@
throw new AssertionError(e);
}
}
+
+ private static HostnameVerifier createInsecureHostnameVerifier() {
+ return new HostnameVerifier() {
+ @Override public boolean verify(String s, SSLSession sslSession) {
+ return true;
+ }
+ };
+ }
+
+ private static void enableHttp2FrameLogging() {
+ Logger logger = Logger.getLogger(Http20Draft16.class.getName() + "$FrameLogger");
+ logger.setLevel(Level.FINE);
+ ConsoleHandler handler = new ConsoleHandler();
+ handler.setLevel(Level.FINE);
+ handler.setFormatter(new SimpleFormatter() {
+ @Override public String format(LogRecord record) {
+ return String.format("%s%n", record.getMessage());
+ }
+ });
+ logger.addHandler(handler);
+ }
}
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 6a5b972..0cc065c 100644
--- a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
+++ b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
@@ -16,8 +16,9 @@
package com.squareup.okhttp.curl;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import java.io.IOException;
-import okio.OkBuffer;
+import okio.Buffer;
import org.junit.Test;
import static com.squareup.okhttp.curl.Main.fromArgs;
@@ -32,16 +33,16 @@
assertNull(request.body());
}
- @Test public void put() {
+ @Test public void put() throws IOException {
Request request = fromArgs("-X", "PUT", "http://example.com").createRequest();
assertEquals("PUT", request.method());
assertEquals("http://example.com", request.urlString());
- assertNull(request.body());
+ assertEquals(0, request.body().contentLength());
}
@Test public void dataPost() {
Request request = fromArgs("-d", "foo", "http://example.com").createRequest();
- Request.Body body = request.body();
+ 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());
@@ -50,7 +51,7 @@
@Test public void dataPut() {
Request request = fromArgs("-d", "foo", "-X", "PUT", "http://example.com").createRequest();
- Request.Body body = request.body();
+ 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());
@@ -60,7 +61,7 @@
@Test public void contentTypeHeader() {
Request request = fromArgs("-d", "foo", "-H", "Content-Type: application/json",
"http://example.com").createRequest();
- Request.Body body = request.body();
+ RequestBody body = request.body();
assertEquals("POST", request.method());
assertEquals("http://example.com", request.urlString());
assertEquals("application/json; charset=utf-8", body.contentType().toString());
@@ -83,12 +84,17 @@
assertNull(request.body());
}
- private static String bodyAsString(Request.Body body) {
+ @Test public void headerSplitWithDate() {
+ Request request = fromArgs("-H", "If-Modified-Since: Mon, 18 Aug 2014 15:16:06 GMT",
+ "http://example.com").createRequest();
+ assertEquals("Mon, 18 Aug 2014 15:16:06 GMT", request.header("If-Modified-Since"));
+ }
+
+ private static String bodyAsString(RequestBody body) {
try {
- OkBuffer buffer = new OkBuffer();
+ Buffer buffer = new Buffer();
body.writeTo(buffer);
- return new String(buffer.readByteString(buffer.size()).toByteArray(),
- body.contentType().charset());
+ return buffer.readString(body.contentType().charset());
} catch (IOException e) {
throw new RuntimeException(e);
}
diff --git a/okhttp-android-support/pom.xml b/okhttp-android-support/pom.xml
new file mode 100644
index 0000000..f00d51a
--- /dev/null
+++ b/okhttp-android-support/pom.xml
@@ -0,0 +1,50 @@
+<?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.3.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>okhttp-android-support</artifactId>
+ <name>OkHttp Android Platform Support</name>
+ <description>Classes to support the Android platform's use of OkHttp (not required for most developers).</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-urlconnection</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>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <excludePackageNames>com.squareup.okhttp.internal.*</excludePackageNames>
+ <links>
+ <link>http://square.github.io/okhttp/javadoc/</link>
+ </links>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/okhttp-android-support/src/main/java/com/squareup/okhttp/AndroidInternal.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/AndroidInternal.java
new file mode 100644
index 0000000..eeaf554
--- /dev/null
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/AndroidInternal.java
@@ -0,0 +1,45 @@
+/*
+ * 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.huc.CacheAdapter;
+
+import java.net.ResponseCache;
+
+/**
+ * Back doors to enable the use of OkHttp within the Android platform libraries. OkHttp is used to
+ * provide the default {@link java.net.HttpURLConnection} / {@link javax.net.ssl.HttpsURLConnection}
+ * implementation including support for a custom {@link ResponseCache}.
+ */
+public class AndroidInternal {
+
+ private AndroidInternal() {
+ }
+
+ /** Sets the response cache to be used to read and write cached responses. */
+ public static void setResponseCache(OkUrlFactory okUrlFactory, ResponseCache responseCache) {
+ OkHttpClient client = okUrlFactory.client();
+ if (responseCache instanceof OkCacheContainer) {
+ // Avoid adding layers of wrappers. Rather than wrap the ResponseCache in yet another layer to
+ // make the ResponseCache look like an InternalCache, we can unwrap the Cache instead.
+ // This means that Cache stats will be correctly updated.
+ OkCacheContainer okCacheContainer = (OkCacheContainer) responseCache;
+ client.setCache(okCacheContainer.getCache());
+ } else {
+ client.setInternalCache(responseCache != null ? new CacheAdapter(responseCache) : null);
+ }
+ }
+}
diff --git a/okhttp-android-support/src/main/java/com/squareup/okhttp/AndroidShimResponseCache.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/AndroidShimResponseCache.java
new file mode 100644
index 0000000..488d3d6
--- /dev/null
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/AndroidShimResponseCache.java
@@ -0,0 +1,145 @@
+/*
+ * 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.huc.JavaApiConverter;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A class provided for use by Android so that it can continue supporting a {@link ResponseCache}
+ * with stats.
+ */
+public class AndroidShimResponseCache extends ResponseCache {
+
+ private final Cache delegate;
+
+ private AndroidShimResponseCache(Cache delegate) {
+ this.delegate = delegate;
+ }
+
+ public static AndroidShimResponseCache create(File directory, long maxSize) throws IOException {
+ Cache cache = new Cache(directory, maxSize);
+ return new AndroidShimResponseCache(cache);
+ }
+
+ public boolean isEquivalent(File directory, long maxSize) {
+ Cache installedCache = getCache();
+ return (installedCache.getDirectory().equals(directory)
+ && installedCache.getMaxSize() == maxSize
+ && !installedCache.isClosed());
+ }
+
+ public Cache getCache() {
+ return delegate;
+ }
+
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) throws IOException {
+ Request okRequest = JavaApiConverter.createOkRequest(uri, requestMethod, requestHeaders);
+ Response okResponse = delegate.internalCache.get(okRequest);
+ if (okResponse == null) {
+ return null;
+ }
+ return JavaApiConverter.createJavaCacheResponse(okResponse);
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+ Response okResponse = JavaApiConverter.createOkResponse(uri, urlConnection);
+ com.squareup.okhttp.internal.http.CacheRequest okCacheRequest =
+ delegate.internalCache.put(okResponse);
+ if (okCacheRequest == null) {
+ return null;
+ }
+ return JavaApiConverter.createJavaCacheRequest(okCacheRequest);
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the {@link #maxSize} if a background
+ * deletion is pending.
+ */
+ public long size() throws IOException {
+ return delegate.getSize();
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public long maxSize() {
+ return delegate.getMaxSize();
+ }
+
+ /**
+ * Force buffered operations to the filesystem. This ensures that responses
+ * written to the cache will be available the next time the cache is opened,
+ * even if this process is killed.
+ */
+ public void flush() throws IOException {
+ delegate.flush();
+ }
+
+ /**
+ * Returns the number of HTTP requests that required the network to either
+ * supply a response or validate a locally cached response.
+ */
+ public int getNetworkCount() {
+ return delegate.getNetworkCount();
+ }
+
+ /**
+ * Returns the number of HTTP requests whose response was provided by the
+ * cache. This may include conditional {@code GET} requests that were
+ * validated over the network.
+ */
+ public int getHitCount() {
+ return delegate.getHitCount();
+ }
+
+ /**
+ * Returns the total number of HTTP requests that were made. This includes
+ * both client requests and requests that were made on the client's behalf
+ * to handle a redirects and retries.
+ */
+ public int getRequestCount() {
+ return delegate.getRequestCount();
+ }
+
+ /**
+ * Uninstalls the cache and releases any active resources. Stored contents
+ * will remain on the filesystem.
+ */
+ public void close() throws IOException {
+ delegate.close();
+ }
+
+ /**
+ * Uninstalls the cache and deletes all of its stored contents.
+ */
+ public void delete() throws IOException {
+ delegate.delete();
+ }
+
+}
diff --git a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/OkCacheContainer.java
similarity index 74%
copy from okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
copy to okhttp-android-support/src/main/java/com/squareup/okhttp/OkCacheContainer.java
index ac3de72..d7b62e3 100644
--- a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/OkCacheContainer.java
@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package okio;
+package com.squareup.okhttp;
-public final class OkBufferReadUtf8LineTest extends ReadUtf8LineTest {
- @Override protected BufferedSource newSource(String s) {
- return new OkBuffer().writeUtf8(s);
- }
+/**
+ * An interface that allows OkHttp to detect that a {@link java.net.ResponseCache} contains a
+ * {@link Cache}.
+ */
+public interface OkCacheContainer {
+ Cache getCache();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseCacheAdapter.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/CacheAdapter.java
similarity index 66%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseCacheAdapter.java
rename to okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/CacheAdapter.java
index 9231307..13a34c0 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/ResponseCacheAdapter.java
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/CacheAdapter.java
@@ -13,30 +13,29 @@
* 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.huc;
-import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.internal.InternalCache;
+import com.squareup.okhttp.internal.http.CacheRequest;
+import com.squareup.okhttp.internal.http.CacheStrategy;
import java.io.IOException;
-import java.net.CacheRequest;
+import java.io.OutputStream;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.ResponseCache;
import java.net.URI;
import java.util.List;
import java.util.Map;
+import okio.Okio;
+import okio.Sink;
-/**
- * An adapter from {@link ResponseCache} to {@link com.squareup.okhttp.OkResponseCache}. This class
- * enables OkHttp to continue supporting Java standard response cache implementations.
- */
-public class ResponseCacheAdapter implements OkResponseCache {
-
+/** Adapts {@link ResponseCache} to {@link InternalCache}. */
+public final class CacheAdapter implements InternalCache {
private final ResponseCache delegate;
- public ResponseCacheAdapter(ResponseCache delegate) {
+ public CacheAdapter(ResponseCache delegate) {
this.delegate = delegate;
}
@@ -44,8 +43,7 @@
return delegate;
}
- @Override
- public Response get(Request request) throws IOException {
+ @Override public Response get(Request request) throws IOException {
CacheResponse javaResponse = getJavaCachedResponse(request);
if (javaResponse == null) {
return null;
@@ -53,25 +51,34 @@
return JavaApiConverter.createOkResponse(request, javaResponse);
}
- @Override
- public CacheRequest put(Response response) throws IOException {
+ @Override public CacheRequest put(Response response) throws IOException {
URI uri = response.request().uri();
HttpURLConnection connection = JavaApiConverter.createJavaUrlConnection(response);
- return delegate.put(uri, connection);
+ final java.net.CacheRequest request = delegate.put(uri, connection);
+ if (request == null) {
+ return null;
+ }
+ return new CacheRequest() {
+ @Override public Sink body() throws IOException {
+ OutputStream body = request.getBody();
+ return body != null ? Okio.sink(body) : null;
+ }
+
+ @Override public void abort() {
+ request.abort();
+ }
+ };
}
- @Override
- public boolean maybeRemove(Request request) throws IOException {
+ @Override public void remove(Request request) throws IOException {
// This method is treated as optional and there is no obvious way of implementing it with
// ResponseCache. Removing items from the cache due to modifications made from this client is
// not essential given that modifications could be made from any other client. We have to assume
// that it's ok to keep using the cached data. Otherwise the server shouldn't declare it as
// cacheable or the client should be careful about caching it.
- return false;
}
- @Override
- public void update(Response cached, Response network) throws IOException {
+ @Override public void update(Response cached, Response network) throws IOException {
// This method is treated as optional and there is no obvious way of implementing it with
// ResponseCache. Updating headers is useful if the server changes the metadata for a resource
// (e.g. max age) to extend or truncate the life of that resource in the cache. If the metadata
@@ -79,14 +86,12 @@
// with the original cached response.
}
- @Override
- public void trackConditionalCacheHit() {
- // This method is treated as optional.
+ @Override public void trackConditionalCacheHit() {
+ // This method is optional.
}
- @Override
- public void trackResponse(ResponseSource source) {
- // This method is treated as optional.
+ @Override public void trackResponse(CacheStrategy cacheStrategy) {
+ // This method is optional.
}
/**
@@ -97,5 +102,4 @@
Map<String, List<String>> headers = JavaApiConverter.extractJavaHeaders(request);
return delegate.get(request.uri(), request.method(), headers);
}
-
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/JavaApiConverter.java b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
similarity index 83%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/JavaApiConverter.java
rename to okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
index 24ecea5..3494e1a 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/JavaApiConverter.java
+++ b/okhttp-android-support/src/main/java/com/squareup/okhttp/internal/huc/JavaApiConverter.java
@@ -13,15 +13,18 @@
* 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.huc;
import com.squareup.okhttp.Handshake;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.CacheRequest;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.StatusLine;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -40,6 +43,9 @@
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
/**
* Helper methods that convert between Java and OkHttp representations.
@@ -65,18 +71,17 @@
okResponseBuilder.request(okRequest);
// Status line
- String statusLine = extractStatusLine(httpUrlConnection);
- okResponseBuilder.statusLine(statusLine);
+ StatusLine statusLine = StatusLine.parse(extractStatusLine(httpUrlConnection));
+ okResponseBuilder.protocol(statusLine.protocol);
+ okResponseBuilder.code(statusLine.code);
+ okResponseBuilder.message(statusLine.message);
// Response headers
Headers okHeaders = extractOkResponseHeaders(httpUrlConnection);
okResponseBuilder.headers(okHeaders);
- // Meta data: Defaulted
- okResponseBuilder.setResponseSource(ResponseSource.NETWORK);
-
// Response body
- Response.Body okBody = createOkBody(okHeaders, urlConnection.getInputStream());
+ ResponseBody okBody = createOkBody(urlConnection);
okResponseBuilder.body(okBody);
// Handle SSL handshake information as needed.
@@ -113,17 +118,17 @@
okResponseBuilder.request(request);
// Status line: Java has this as one of the headers.
- okResponseBuilder.statusLine(extractStatusLine(javaResponse));
+ StatusLine statusLine = StatusLine.parse(extractStatusLine(javaResponse));
+ okResponseBuilder.protocol(statusLine.protocol);
+ okResponseBuilder.code(statusLine.code);
+ okResponseBuilder.message(statusLine.message);
// Response headers
Headers okHeaders = extractOkHeaders(javaResponse);
okResponseBuilder.headers(okHeaders);
- // Meta data: Defaulted
- okResponseBuilder.setResponseSource(ResponseSource.CACHE);
-
// Response body
- Response.Body okBody = createOkBody(okHeaders, javaResponse.getBody());
+ ResponseBody okBody = createOkBody(okHeaders, javaResponse);
okResponseBuilder.body(okBody);
// Handle SSL handshake information as needed.
@@ -176,7 +181,7 @@
*/
public static CacheResponse createJavaCacheResponse(final Response response) {
final Headers headers = response.headers();
- final Response.Body body = response.body();
+ final ResponseBody body = response.body();
if (response.request().isHttps()) {
final Handshake handshake = response.handshake();
return new SecureCacheResponse() {
@@ -216,7 +221,7 @@
@Override
public Map<String, List<String>> getHeaders() throws IOException {
// Java requires that the entry with a null key be the status line.
- return OkHeaders.toMultimap(headers, response.statusLine());
+ return OkHeaders.toMultimap(headers, StatusLine.get(response).toString());
}
@Override
@@ -230,7 +235,7 @@
@Override
public Map<String, List<String>> getHeaders() throws IOException {
// Java requires that the entry with a null key be the status line.
- return OkHeaders.toMultimap(headers, response.statusLine());
+ return OkHeaders.toMultimap(headers, StatusLine.get(response).toString());
}
@Override
@@ -242,6 +247,23 @@
}
}
+ public static java.net.CacheRequest createJavaCacheRequest(final CacheRequest okCacheRequest) {
+ return new java.net.CacheRequest() {
+ @Override
+ public void abort() {
+ okCacheRequest.abort();
+ }
+ @Override
+ public OutputStream getBody() throws IOException {
+ Sink body = okCacheRequest.body();
+ if (body == null) {
+ return null;
+ }
+ return Okio.buffer(body).outputStream();
+ }
+ };
+ }
+
/**
* Creates an {@link java.net.HttpURLConnection} of the correct subclass from the supplied OkHttp
* {@link Response}.
@@ -336,13 +358,10 @@
/**
* Creates an OkHttp Response.Body containing the supplied information.
*/
- private static Response.Body createOkBody(final Headers okHeaders, final InputStream body) {
- return new Response.Body() {
-
- @Override
- public boolean ready() throws IOException {
- return true;
- }
+ private static ResponseBody createOkBody(final Headers okHeaders,
+ final CacheResponse cacheResponse) {
+ return new ResponseBody() {
+ private BufferedSource body;
@Override
public MediaType contentType() {
@@ -354,9 +373,39 @@
public long contentLength() {
return OkHeaders.contentLength(okHeaders);
}
+ @Override public BufferedSource source() throws IOException {
+ if (body == null) {
+ InputStream is = cacheResponse.getBody();
+ body = Okio.buffer(Okio.source(is));
+ }
+ return body;
+ }
+ };
+ }
- @Override
- public InputStream byteStream() {
+ /**
+ * Creates an OkHttp Response.Body containing the supplied information.
+ */
+ private static ResponseBody createOkBody(final URLConnection urlConnection) {
+ if (!urlConnection.getDoInput()) {
+ return null;
+ }
+ return new ResponseBody() {
+ private BufferedSource body;
+
+ @Override public MediaType contentType() {
+ String contentTypeHeader = urlConnection.getContentType();
+ return contentTypeHeader == null ? null : MediaType.parse(contentTypeHeader);
+ }
+ @Override public long contentLength() {
+ String s = urlConnection.getHeaderField("Content-Length");
+ return stringToLong(s);
+ }
+ @Override public BufferedSource source() throws IOException {
+ if (body == null) {
+ InputStream is = urlConnection.getInputStream();
+ body = Okio.buffer(Okio.source(is));
+ }
return body;
}
};
@@ -383,7 +432,9 @@
// Configure URLConnection inherited fields.
this.connected = true;
- this.doOutput = response.body() == null;
+ this.doOutput = request.body() != null;
+ this.doInput = true;
+ this.useCaches = true;
// Configure HttpUrlConnection inherited fields.
this.method = request.method();
@@ -482,19 +533,21 @@
throw new IllegalArgumentException("Invalid header index: " + position);
}
if (position == 0) {
- return response.statusLine();
+ return StatusLine.get(response).toString();
}
return response.headers().value(position - 1);
}
@Override
public String getHeaderField(String fieldName) {
- return fieldName == null ? response.statusLine() : response.headers().get(fieldName);
+ return fieldName == null
+ ? StatusLine.get(response).toString()
+ : response.headers().get(fieldName);
}
@Override
public Map<String, List<String>> getHeaderFields() {
- return OkHeaders.toMultimap(response.headers(), response.statusLine());
+ return OkHeaders.toMultimap(response.headers(), StatusLine.get(response).toString());
}
@Override
@@ -504,7 +557,7 @@
@Override
public String getResponseMessage() throws IOException {
- return response.statusMessage();
+ return response.message();
}
@Override
@@ -572,7 +625,7 @@
@Override
public boolean getDoInput() {
- return true;
+ return doInput;
}
@Override
@@ -582,7 +635,7 @@
@Override
public boolean getDoOutput() {
- return request.body() != null;
+ return doOutput;
}
@Override
@@ -612,7 +665,7 @@
@Override
public long getIfModifiedSince() {
- return 0;
+ return stringToLong(request.headers().get("If-Modified-Since"));
}
@Override
@@ -655,9 +708,21 @@
throw throwRequestSslAccessException();
}
+ // ANDROID-BEGIN
+ // @Override public long getContentLengthLong() {
+ // return delegate.getContentLengthLong();
+ // }
+ // ANDROID-END
+
@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
}
private static RuntimeException throwRequestModificationException() {
@@ -680,4 +745,12 @@
return elements == null ? Collections.<T>emptyList() : Util.immutableList(elements);
}
+ private static long stringToLong(String s) {
+ if (s == null) return -1;
+ try {
+ return Long.parseLong(s);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
}
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/AbstractResponseCache.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/AbstractResponseCache.java
new file mode 100644
index 0000000..2a59d22
--- /dev/null
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/AbstractResponseCache.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+public class AbstractResponseCache extends ResponseCache {
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) throws IOException {
+ return null;
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ return null;
+ }
+
+ public static URI toUri(URL serverUrl) {
+ try {
+ return serverUrl.toURI();
+ } catch (URISyntaxException e) {
+ throw new AssertionError(e);
+ }
+ }
+}
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCache.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCache.java
new file mode 100644
index 0000000..30d965c
--- /dev/null
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCache.java
@@ -0,0 +1,191 @@
+/*
+ * 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.android;
+
+import com.squareup.okhttp.Cache;
+import com.squareup.okhttp.AndroidShimResponseCache;
+import com.squareup.okhttp.OkCacheContainer;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A copy of android.net.http.HttpResponseCache taken from AOSP. Android need to keep this code
+ * working somehow. Dependencies on com.squareup.okhttp are com.android.okhttp on Android.
+ */
+/* <p>This class exists in okhttp-android-support to help keep the API as it always has been on
+ * Android. The public API cannot be changed. This class delegates to
+ * {@link com.squareup.okhttp.AndroidShimResponseCache}, a class that exists in a package that
+ * enables it to interact with non-public OkHttp classes.
+ */
+public final class HttpResponseCache extends ResponseCache implements Closeable, OkCacheContainer {
+
+ private AndroidShimResponseCache shimResponseCache;
+
+ private HttpResponseCache(AndroidShimResponseCache shimResponseCache) {
+ this.shimResponseCache = shimResponseCache;
+ }
+
+ /**
+ * Returns the currently-installed {@code HttpResponseCache}, or null if
+ * there is no cache installed or it is not a {@code HttpResponseCache}.
+ */
+ public static HttpResponseCache getInstalled() {
+ ResponseCache installed = ResponseCache.getDefault();
+ if (installed instanceof HttpResponseCache) {
+ return (HttpResponseCache) installed;
+ }
+ return null;
+ }
+
+ /**
+ * Creates a new HTTP response cache and sets it as the system default cache.
+ *
+ * @param directory the directory to hold cache data.
+ * @param maxSize the maximum size of the cache in bytes.
+ * @return the newly-installed cache
+ * @throws java.io.IOException if {@code directory} cannot be used for this cache.
+ * Most applications should respond to this exception by logging a
+ * warning.
+ */
+ public static synchronized HttpResponseCache install(File directory, long maxSize) throws
+ IOException {
+ ResponseCache installed = ResponseCache.getDefault();
+
+ if (installed instanceof HttpResponseCache) {
+ HttpResponseCache installedResponseCache = (HttpResponseCache) installed;
+ // don't close and reopen if an equivalent cache is already installed
+ AndroidShimResponseCache trueResponseCache = installedResponseCache.shimResponseCache;
+ if (trueResponseCache.isEquivalent(directory, maxSize)) {
+ return installedResponseCache;
+ } else {
+ // The HttpResponseCache that owns this object is about to be replaced.
+ trueResponseCache.close();
+ }
+ }
+
+ AndroidShimResponseCache trueResponseCache =
+ AndroidShimResponseCache.create(directory, maxSize);
+ HttpResponseCache newResponseCache = new HttpResponseCache(trueResponseCache);
+ ResponseCache.setDefault(newResponseCache);
+ return newResponseCache;
+ }
+
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) throws IOException {
+ return shimResponseCache.get(uri, requestMethod, requestHeaders);
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+ return shimResponseCache.put(uri, urlConnection);
+ }
+
+ /**
+ * Returns the number of bytes currently being used to store the values in
+ * this cache. This may be greater than the {@link #maxSize} if a background
+ * deletion is pending.
+ */
+ public long size() {
+ try {
+ return shimResponseCache.size();
+ } catch (IOException e) {
+ // This can occur if the cache failed to lazily initialize. Return -1 to mean "unknown".
+ return -1;
+ }
+ }
+
+ /**
+ * Returns the maximum number of bytes that this cache should use to store
+ * its data.
+ */
+ public long maxSize() {
+ return shimResponseCache.maxSize();
+ }
+
+ /**
+ * Force buffered operations to the filesystem. This ensures that responses
+ * written to the cache will be available the next time the cache is opened,
+ * even if this process is killed.
+ */
+ public void flush() {
+ try {
+ shimResponseCache.flush();
+ } catch (IOException ignored) {
+ }
+ }
+
+ /**
+ * Returns the number of HTTP requests that required the network to either
+ * supply a response or validate a locally cached response.
+ */
+ public int getNetworkCount() {
+ return shimResponseCache.getNetworkCount();
+ }
+
+ /**
+ * Returns the number of HTTP requests whose response was provided by the
+ * cache. This may include conditional {@code GET} requests that were
+ * validated over the network.
+ */
+ public int getHitCount() {
+ return shimResponseCache.getHitCount();
+ }
+
+ /**
+ * Returns the total number of HTTP requests that were made. This includes
+ * both client requests and requests that were made on the client's behalf
+ * to handle a redirects and retries.
+ */
+ public int getRequestCount() {
+ return shimResponseCache.getRequestCount();
+ }
+
+ /**
+ * Uninstalls the cache and releases any active resources. Stored contents
+ * will remain on the filesystem.
+ */
+ @Override public void close() throws IOException {
+ if (ResponseCache.getDefault() == this) {
+ ResponseCache.setDefault(null);
+ }
+ shimResponseCache.close();
+ }
+
+ /**
+ * Uninstalls the cache and deletes all of its stored contents.
+ */
+ public void delete() throws IOException {
+ if (ResponseCache.getDefault() == this) {
+ ResponseCache.setDefault(null);
+ }
+ shimResponseCache.delete();
+ }
+
+ @Override
+ public Cache getCache() {
+ return shimResponseCache.getCache();
+ }
+
+}
diff --git a/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java
new file mode 100644
index 0000000..c349790
--- /dev/null
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/android/HttpResponseCacheTest.java
@@ -0,0 +1,181 @@
+/*
+ * 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.android;
+
+import com.squareup.okhttp.AndroidInternal;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+/**
+ * A port of Android's android.net.http.HttpResponseCacheTest to JUnit4.
+ */
+public final class HttpResponseCacheTest {
+
+ @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
+ @Rule public MockWebServerRule serverRule = new MockWebServerRule();
+
+ private File cacheDir;
+ private MockWebServer server;
+ private OkUrlFactory client;
+
+ @Before public void setUp() throws Exception {
+ server = serverRule.get();
+ cacheDir = cacheRule.getRoot();
+ client = new OkUrlFactory(new OkHttpClient());
+ }
+
+ @After public void tearDown() throws Exception {
+ ResponseCache.setDefault(null);
+ }
+
+ @Test public void install() throws Exception {
+ HttpResponseCache installed = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ assertNotNull(installed);
+ assertSame(installed, ResponseCache.getDefault());
+ assertSame(installed, HttpResponseCache.getDefault());
+ }
+
+ @Test public void secondEquivalentInstallDoesNothing() throws Exception {
+ HttpResponseCache first = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ HttpResponseCache another = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ assertSame(first, another);
+ }
+
+ @Test public void installClosesPreviouslyInstalled() throws Exception {
+ HttpResponseCache first = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ initializeCache(first);
+
+ HttpResponseCache another = HttpResponseCache.install(cacheDir, 8 * 1024 * 1024);
+ initializeCache(another);
+
+ assertNotSame(first, another);
+ try {
+ first.flush();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void getInstalledWithWrongTypeInstalled() {
+ ResponseCache.setDefault(new ResponseCache() {
+ @Override
+ public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) {
+ return null;
+ }
+
+ @Override
+ public CacheRequest put(URI uri, URLConnection connection) {
+ return null;
+ }
+ });
+ assertNull(HttpResponseCache.getInstalled());
+ }
+
+ @Test public void closeCloses() throws Exception {
+ HttpResponseCache cache = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ initializeCache(cache);
+
+ cache.close();
+ try {
+ cache.flush();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void closeUninstalls() throws Exception {
+ HttpResponseCache cache = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ cache.close();
+ assertNull(ResponseCache.getDefault());
+ }
+
+ @Test public void deleteUninstalls() throws Exception {
+ HttpResponseCache cache = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+ cache.delete();
+ assertNull(ResponseCache.getDefault());
+ }
+
+ /**
+ * Make sure that statistics tracking are wired all the way through the
+ * wrapper class. http://code.google.com/p/android/issues/detail?id=25418
+ */
+ @Test public void statisticsTracking() throws Exception {
+ HttpResponseCache cache = HttpResponseCache.install(cacheDir, 10 * 1024 * 1024);
+
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+
+ URLConnection c1 = openUrl(server.getUrl("/"));
+
+ InputStream inputStream = c1.getInputStream();
+ assertEquals('A', inputStream.read());
+ inputStream.close();
+ assertEquals(1, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+
+ URLConnection c2 = openUrl(server.getUrl("/"));
+ assertEquals('A', c2.getInputStream().read());
+
+ URLConnection c3 = openUrl(server.getUrl("/"));
+ assertEquals('A', c3.getInputStream().read());
+ assertEquals(3, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(2, cache.getHitCount());
+ }
+
+ // This mimics the Android HttpHandler, which is found in the com.squareup.okhttp package.
+ private URLConnection openUrl(URL url) {
+ ResponseCache responseCache = ResponseCache.getDefault();
+ AndroidInternal.setResponseCache(client, responseCache);
+ return client.open(url);
+ }
+
+ private void initializeCache(HttpResponseCache cache) {
+ // Ensure the cache is initialized, otherwise various methods are no-ops.
+ cache.size();
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheAdapterTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java
similarity index 72%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheAdapterTest.java
rename to okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java
index 8d5e152..4cca79e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheAdapterTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/CacheAdapterTest.java
@@ -13,45 +13,44 @@
* 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.huc;
+import com.squareup.okhttp.AbstractResponseCache;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
+import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
import java.io.IOException;
import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
import java.net.ResponseCache;
import java.net.URI;
-import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import okio.Buffer;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
/**
- * A white-box test for {@link ResponseCacheAdapter}. See also:
+ * A white-box test for {@link CacheAdapter}. See also:
* <ul>
* <li>{@link ResponseCacheTest} for black-box tests that check that {@link ResponseCache}
* classes are called correctly by OkHttp.</li>
@@ -59,8 +58,7 @@
* logic. </li>
* </ul>
*/
-public class ResponseCacheAdapterTest {
-
+public class CacheAdapterTest {
private static final SSLContext sslContext = SslContextBuilder.localhost();
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
@@ -90,7 +88,7 @@
final URL serverUrl = configureServer(new MockResponse());
assertEquals("http", serverUrl.getProtocol());
- ResponseCache responseCache = new NoOpResponseCache() {
+ ResponseCache responseCache = new AbstractResponseCache() {
@Override
public CacheResponse get(URI uri, String method, Map<String, List<String>> headers) throws IOException {
assertEquals(toUri(serverUrl), uri);
@@ -100,9 +98,9 @@
return null;
}
};
- client.setResponseCache(responseCache);
+ Internal.instance.setCache(client, new CacheAdapter(responseCache));
- connection = client.open(serverUrl);
+ connection = new OkUrlFactory(client).open(serverUrl);
connection.setRequestProperty("key1", "value1");
executeGet(connection);
@@ -112,9 +110,8 @@
final URL serverUrl = configureHttpsServer(new MockResponse());
assertEquals("https", serverUrl.getProtocol());
- ResponseCache responseCache = new NoOpResponseCache() {
- @Override
- public CacheResponse get(URI uri, String method, Map<String, List<String>> headers)
+ ResponseCache responseCache = new AbstractResponseCache() {
+ @Override public CacheResponse get(URI uri, String method, Map<String, List<String>> headers)
throws IOException {
assertEquals("https", uri.getScheme());
assertEquals(toUri(serverUrl), uri);
@@ -124,11 +121,11 @@
return null;
}
};
- client.setResponseCache(responseCache);
+ Internal.instance.setCache(client, new CacheAdapter(responseCache));
client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- connection = client.open(serverUrl);
+ connection = new OkUrlFactory(client).open(serverUrl);
connection.setRequestProperty("key1", "value1");
executeGet(connection);
@@ -136,28 +133,29 @@
@Test public void put_httpGet() throws Exception {
final String statusLine = "HTTP/1.1 200 Fantastic";
+ final byte[] response = "ResponseString".getBytes(StandardCharsets.UTF_8);
final URL serverUrl = configureServer(
new MockResponse()
.setStatus(statusLine)
- .addHeader("A", "c"));
+ .addHeader("A", "c")
+ .setBody(new Buffer().write(response)));
- ResponseCache responseCache = new NoOpResponseCache() {
- @Override
- public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
- assertTrue(urlConnection instanceof HttpURLConnection);
- assertFalse(urlConnection instanceof HttpsURLConnection);
+ ResponseCache responseCache = new AbstractResponseCache() {
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ assertTrue(connection instanceof HttpURLConnection);
+ assertFalse(connection instanceof HttpsURLConnection);
- assertEquals(0, urlConnection.getContentLength());
+ assertEquals(response.length, connection.getContentLength());
- HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
+ HttpURLConnection httpUrlConnection = (HttpURLConnection) connection;
assertEquals("GET", httpUrlConnection.getRequestMethod());
assertTrue(httpUrlConnection.getDoInput());
assertFalse(httpUrlConnection.getDoOutput());
assertEquals("Fantastic", httpUrlConnection.getResponseMessage());
assertEquals(toUri(serverUrl), uri);
- assertEquals(serverUrl, urlConnection.getURL());
- assertEquals("value", urlConnection.getRequestProperty("key"));
+ assertEquals(serverUrl, connection.getURL());
+ assertEquals("value", connection.getRequestProperty("key"));
// Check retrieval by string key.
assertEquals(statusLine, httpUrlConnection.getHeaderField(null));
@@ -167,9 +165,9 @@
return null;
}
};
- client.setResponseCache(responseCache);
+ Internal.instance.setCache(client, new CacheAdapter(responseCache));
- connection = client.open(serverUrl);
+ connection = new OkUrlFactory(client).open(serverUrl);
connection.setRequestProperty("key", "value");
executeGet(connection);
}
@@ -181,23 +179,22 @@
.setStatus(statusLine)
.addHeader("A", "c"));
- ResponseCache responseCache = new NoOpResponseCache() {
- @Override
- public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
- assertTrue(urlConnection instanceof HttpURLConnection);
- assertFalse(urlConnection instanceof HttpsURLConnection);
+ ResponseCache responseCache = new AbstractResponseCache() {
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ assertTrue(connection instanceof HttpURLConnection);
+ assertFalse(connection instanceof HttpsURLConnection);
- assertEquals(0, urlConnection.getContentLength());
+ assertEquals(0, connection.getContentLength());
- HttpURLConnection httpUrlConnection = (HttpURLConnection) urlConnection;
+ HttpURLConnection httpUrlConnection = (HttpURLConnection) connection;
assertEquals("POST", httpUrlConnection.getRequestMethod());
assertTrue(httpUrlConnection.getDoInput());
assertTrue(httpUrlConnection.getDoOutput());
assertEquals("Fantastic", httpUrlConnection.getResponseMessage());
assertEquals(toUri(serverUrl), uri);
- assertEquals(serverUrl, urlConnection.getURL());
- assertEquals("value", urlConnection.getRequestProperty("key"));
+ assertEquals(serverUrl, connection.getURL());
+ assertEquals("value", connection.getRequestProperty("key"));
// Check retrieval by string key.
assertEquals(statusLine, httpUrlConnection.getHeaderField(null));
@@ -207,9 +204,9 @@
return null;
}
};
- client.setResponseCache(responseCache);
+ Internal.instance.setCache(client, new CacheAdapter(responseCache));
- connection = client.open(serverUrl);
+ connection = new OkUrlFactory(client).open(serverUrl);
executePost(connection);
}
@@ -218,15 +215,14 @@
final URL serverUrl = configureHttpsServer(new MockResponse());
assertEquals("https", serverUrl.getProtocol());
- ResponseCache responseCache = new NoOpResponseCache() {
- @Override
- public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
- assertTrue(urlConnection instanceof HttpsURLConnection);
+ ResponseCache responseCache = new AbstractResponseCache() {
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ assertTrue(connection instanceof HttpsURLConnection);
assertEquals(toUri(serverUrl), uri);
- assertEquals(serverUrl, urlConnection.getURL());
+ assertEquals(serverUrl, connection.getURL());
- HttpsURLConnection cacheHttpsUrlConnection = (HttpsURLConnection) urlConnection;
- HttpsURLConnection realHttpsUrlConnection = (HttpsURLConnection) connection;
+ HttpsURLConnection cacheHttpsUrlConnection = (HttpsURLConnection) connection;
+ HttpsURLConnection realHttpsUrlConnection = (HttpsURLConnection) CacheAdapterTest.this.connection;
assertEquals(realHttpsUrlConnection.getCipherSuite(),
cacheHttpsUrlConnection.getCipherSuite());
assertEquals(realHttpsUrlConnection.getPeerPrincipal(),
@@ -240,11 +236,11 @@
return null;
}
};
- client.setResponseCache(responseCache);
+ Internal.instance.setCache(client, new CacheAdapter(responseCache));
client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- connection = client.open(serverUrl);
+ connection = new OkUrlFactory(client).open(serverUrl);
executeGet(connection);
}
@@ -263,37 +259,14 @@
private URL configureServer(MockResponse mockResponse) throws Exception {
server.enqueue(mockResponse);
- server.play();
+ server.start();
return server.getUrl("/");
}
private URL configureHttpsServer(MockResponse mockResponse) throws Exception {
server.useHttps(sslContext.getSocketFactory(), false /* tunnelProxy */);
server.enqueue(mockResponse);
- server.play();
+ server.start();
return server.getUrl("/");
}
-
- private static class NoOpResponseCache extends ResponseCache {
-
- @Override
- public CacheResponse get(URI uri, String s, Map<String, List<String>> stringListMap)
- throws IOException {
- return null;
- }
-
- @Override
- public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
- return null;
- }
- }
-
- private static URI toUri(URL serverUrl) {
- try {
- return serverUrl.toURI();
- } catch (URISyntaxException e) {
- fail(e.getMessage());
- return null;
- }
- }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/JavaApiConverterTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
similarity index 84%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/JavaApiConverterTest.java
rename to okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
index 8a6c536..d5dfcd8 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/JavaApiConverterTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/JavaApiConverterTest.java
@@ -13,24 +13,22 @@
* 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.huc;
import com.squareup.okhttp.Handshake;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
+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.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -58,6 +56,12 @@
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
+import okio.Buffer;
+import okio.BufferedSource;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@@ -68,9 +72,6 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-/**
- * Tests for {@link JavaApiConverter}.
- */
public class JavaApiConverterTest {
// $ openssl req -x509 -nodes -days 36500 -subj '/CN=localhost' -config ./cert.cnf \
@@ -108,14 +109,13 @@
}
};
- private MockWebServer server;
+ @Rule public MockWebServerRule server = new MockWebServerRule();
private OkHttpClient client;
private HttpURLConnection connection;
@Before public void setUp() throws Exception {
- server = new MockWebServer();
client = new OkHttpClient();
}
@@ -123,7 +123,6 @@
if (connection != null) {
connection.disconnect();
}
- server.shutdown();
}
@Test public void createOkResponse_fromOkHttpUrlConnection() throws Exception {
@@ -183,10 +182,11 @@
assertEquals("GET", request.method());
// Check the response
- assertEquals(statusLine, response.statusLine());
+ assertEquals(Protocol.HTTP_1_1, response.protocol());
+ assertEquals(200, response.code());
+ assertEquals("Fantastic", response.message());
Headers okResponseHeaders = response.headers();
assertEquals("baz", okResponseHeaders.get("xyzzy"));
- assertEquals(body, response.body().string());
if (isSecure) {
Handshake handshake = response.handshake();
assertNotNull(handshake);
@@ -201,23 +201,22 @@
} else {
assertNull(response.handshake());
}
+ assertEquals(body, response.body().string());
}
@Test public void createOkResponse_fromCacheResponse() throws Exception {
final String statusLine = "HTTP/1.1 200 Fantastic";
URI uri = new URI("http://foo/bar");
- Request request = new Request.Builder().url(uri.toURL()).method("GET", null).build();
+ Request request = new Request.Builder().url(uri.toURL()).build();
CacheResponse cacheResponse = new CacheResponse() {
- @Override
- public Map<String, List<String>> getHeaders() throws IOException {
- Map<String, List<String>> headers = new HashMap<String, List<String>>();
+ @Override public Map<String, List<String>> getHeaders() throws IOException {
+ Map<String, List<String>> headers = new HashMap<>();
headers.put(null, Collections.singletonList(statusLine));
headers.put("xyzzy", Arrays.asList("bar", "baz"));
return headers;
}
- @Override
- public InputStream getBody() throws IOException {
+ @Override public InputStream getBody() throws IOException {
return new ByteArrayInputStream("HelloWorld".getBytes(StandardCharsets.UTF_8));
}
};
@@ -225,7 +224,9 @@
Response response = JavaApiConverter.createOkResponse(request, cacheResponse);
assertSame(request, response.request());
- assertNotNullAndEquals(statusLine, response.statusLine());
+ assertEquals(Protocol.HTTP_1_1, response.protocol());
+ assertEquals(200, response.code());
+ assertEquals("Fantastic", response.message());
Headers okResponseHeaders = response.headers();
assertEquals("baz", okResponseHeaders.get("xyzzy"));
assertEquals("HelloWorld", response.body().string());
@@ -239,43 +240,36 @@
final Principal serverPrincipal = SERVER_CERT.getSubjectX500Principal();
final List<Certificate> serverCertificates = Arrays.<Certificate>asList(SERVER_CERT);
URI uri = new URI("https://foo/bar");
- Request request = new Request.Builder().url(uri.toURL()).method("GET", null).build();
+ Request request = new Request.Builder().url(uri.toURL()).build();
SecureCacheResponse cacheResponse = new SecureCacheResponse() {
- @Override
- public Map<String, List<String>> getHeaders() throws IOException {
- Map<String, List<String>> headers = new HashMap<String, List<String>>();
+ @Override public Map<String, List<String>> getHeaders() throws IOException {
+ Map<String, List<String>> headers = new HashMap<>();
headers.put(null, Collections.singletonList(statusLine));
headers.put("xyzzy", Arrays.asList("bar", "baz"));
return headers;
}
- @Override
- public InputStream getBody() throws IOException {
+ @Override public InputStream getBody() throws IOException {
return new ByteArrayInputStream("HelloWorld".getBytes(StandardCharsets.UTF_8));
}
- @Override
- public String getCipherSuite() {
+ @Override public String getCipherSuite() {
return "SuperSecure";
}
- @Override
- public List<Certificate> getLocalCertificateChain() {
+ @Override public List<Certificate> getLocalCertificateChain() {
return localCertificates;
}
- @Override
- public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
+ @Override public List<Certificate> getServerCertificateChain() throws SSLPeerUnverifiedException {
return serverCertificates;
}
- @Override
- public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
+ @Override public Principal getPeerPrincipal() throws SSLPeerUnverifiedException {
return serverPrincipal;
}
- @Override
- public Principal getLocalPrincipal() {
+ @Override public Principal getLocalPrincipal() {
return localPrincipal;
}
};
@@ -283,7 +277,9 @@
Response response = JavaApiConverter.createOkResponse(request, cacheResponse);
assertSame(request, response.request());
- assertNotNullAndEquals(statusLine, response.statusLine());
+ assertEquals(Protocol.HTTP_1_1, response.protocol());
+ assertEquals(200, response.code());
+ assertEquals("Fantastic", response.message());
Headers okResponseHeaders = response.headers();
assertEquals("baz", okResponseHeaders.get("xyzzy"));
assertEquals("HelloWorld", response.body().string());
@@ -304,7 +300,6 @@
Request request = JavaApiConverter.createOkRequest(uri, "POST", javaRequestHeaders);
assertFalse(request.isHttps());
assertEquals(uri, request.uri());
- assertNull(request.body());
Headers okRequestHeaders = request.headers();
assertEquals(0, okRequestHeaders.size());
assertEquals("POST", request.method());
@@ -313,12 +308,11 @@
@Test public void createOkRequest_nonNullRequestHeaders() throws Exception {
URI uri = new URI("https://foo/bar");
- Map<String,List<String>> javaRequestHeaders = new HashMap<String, List<String>>();
+ Map<String,List<String>> javaRequestHeaders = new HashMap<>();
javaRequestHeaders.put("Foo", Arrays.asList("Bar"));
Request request = JavaApiConverter.createOkRequest(uri, "POST", javaRequestHeaders);
assertTrue(request.isHttps());
assertEquals(uri, request.uri());
- assertNull(request.body());
Headers okRequestHeaders = request.headers();
assertEquals(1, okRequestHeaders.size());
assertEquals("Bar", okRequestHeaders.get("Foo"));
@@ -332,13 +326,12 @@
@Test public void createOkRequest_nullRequestHeaderKey() throws Exception {
URI uri = new URI("https://foo/bar");
- Map<String,List<String>> javaRequestHeaders = new HashMap<String, List<String>>();
+ Map<String,List<String>> javaRequestHeaders = new HashMap<>();
javaRequestHeaders.put(null, Arrays.asList("GET / HTTP 1.1"));
javaRequestHeaders.put("Foo", Arrays.asList("Bar"));
Request request = JavaApiConverter.createOkRequest(uri, "POST", javaRequestHeaders);
assertTrue(request.isHttps());
assertEquals(uri, request.uri());
- assertNull(request.body());
Headers okRequestHeaders = request.headers();
assertEquals(1, okRequestHeaders.size());
assertEquals("Bar", okRequestHeaders.get("Foo"));
@@ -425,12 +418,12 @@
}
@Test public void createJavaUrlConnection_responseHeadersOk() throws Exception {
- final String statusLine = "HTTP/1.1 200 Fantastic";
- Response.Body responseBody =
- createResponseBody("text/plain", "BodyText".getBytes(StandardCharsets.UTF_8));
+ ResponseBody responseBody = createResponseBody("BodyText");
Response okResponse = new Response.Builder()
.request(createArbitraryOkRequest())
- .statusLine(statusLine)
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Fantastic")
.addHeader("A", "c")
.addHeader("B", "d")
.addHeader("A", "e")
@@ -444,20 +437,20 @@
assertEquals(responseBody.contentLength(), httpUrlConnection.getContentLength());
// Check retrieval by string key.
- assertEquals(statusLine, httpUrlConnection.getHeaderField(null));
+ assertEquals("HTTP/1.1 200 Fantastic", httpUrlConnection.getHeaderField(null));
assertEquals("e", httpUrlConnection.getHeaderField("A"));
// The RI and OkHttp supports case-insensitive matching for this method.
assertEquals("e", httpUrlConnection.getHeaderField("a"));
// Check retrieval using a Map.
Map<String, List<String>> responseHeaders = httpUrlConnection.getHeaderFields();
- assertEquals(Arrays.asList(statusLine), responseHeaders.get(null));
+ assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), responseHeaders.get(null));
assertEquals(newSet("c", "e"), newSet(responseHeaders.get("A")));
// OkHttp supports case-insensitive matching here. The RI does not.
assertEquals(newSet("c", "e"), newSet(responseHeaders.get("a")));
// Check the Map iterator contains the expected mappings.
- assertHeadersContainsMapping(responseHeaders, null, statusLine);
+ assertHeadersContainsMapping(responseHeaders, null, "HTTP/1.1 200 Fantastic");
assertHeadersContainsMapping(responseHeaders, "A", "c", "e");
assertHeadersContainsMapping(responseHeaders, "B", "d");
@@ -475,7 +468,7 @@
// Check retrieval of headers by index.
assertEquals(null, httpUrlConnection.getHeaderFieldKey(0));
- assertEquals(statusLine, httpUrlConnection.getHeaderField(0));
+ assertEquals("HTTP/1.1 200 Fantastic", httpUrlConnection.getHeaderField(0));
// After header zero there may be additional entries provided at the beginning or end by the
// implementation. It's probably important that the relative ordering of the headers is
// preserved, particularly if there are multiple value for the same key.
@@ -510,7 +503,7 @@
@Test public void createJavaUrlConnection_accessibleRequestInfo_GET() throws Exception {
Request okRequest = createArbitraryOkRequest().newBuilder()
- .method("GET", null)
+ .get()
.build();
Response okResponse = createArbitraryOkResponse(okRequest);
HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
@@ -522,7 +515,7 @@
@Test public void createJavaUrlConnection_accessibleRequestInfo_POST() throws Exception {
Request okRequest = createArbitraryOkRequest().newBuilder()
- .method("POST", createRequestBody("PostBody"))
+ .post(createRequestBody("PostBody"))
.build();
Response okResponse = createArbitraryOkResponse(okRequest);
HttpURLConnection httpUrlConnection = JavaApiConverter.createJavaUrlConnection(okResponse);
@@ -534,7 +527,7 @@
@Test public void createJavaUrlConnection_https_extraHttpsMethods() throws Exception {
Request okRequest = createArbitraryOkRequest().newBuilder()
- .method("GET", null)
+ .get()
.url("https://secure/request")
.build();
Handshake handshake = Handshake.get("SecureCipher", Arrays.<Certificate>asList(SERVER_CERT),
@@ -577,11 +570,12 @@
Request okRequest =
createArbitraryOkRequest().newBuilder()
.url("http://insecure/request")
- .method("GET", null)
+ .get()
.build();
- String statusLine = "HTTP/1.1 200 Fantastic";
Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
- .statusLine(statusLine)
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Fantastic")
.addHeader("key1", "value1_1")
.addHeader("key2", "value2")
.addHeader("key1", "value1_2")
@@ -591,7 +585,7 @@
assertFalse(javaCacheResponse instanceof SecureCacheResponse);
Map<String, List<String>> javaHeaders = javaCacheResponse.getHeaders();
assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
- assertEquals(Arrays.asList(statusLine), javaHeaders.get(null));
+ assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), javaHeaders.get(null));
assertNull(javaCacheResponse.getBody());
}
@@ -599,13 +593,13 @@
Request okRequest =
createArbitraryOkRequest().newBuilder()
.url("http://insecure/request")
- .method("POST", createRequestBody("RequestBody") )
+ .post(createRequestBody("RequestBody"))
.build();
- String statusLine = "HTTP/1.1 200 Fantastic";
- Response.Body responseBody =
- createResponseBody("text/plain", "ResponseBody".getBytes(StandardCharsets.UTF_8));
+ ResponseBody responseBody = createResponseBody("ResponseBody");
Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
- .statusLine(statusLine)
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Fantastic")
.addHeader("key1", "value1_1")
.addHeader("key2", "value2")
.addHeader("key1", "value1_2")
@@ -615,23 +609,23 @@
assertFalse(javaCacheResponse instanceof SecureCacheResponse);
Map<String, List<String>> javaHeaders = javaCacheResponse.getHeaders();
assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
- assertEquals(Arrays.asList(statusLine), javaHeaders.get(null));
- assertArrayEquals(responseBody.bytes(), readAll(javaCacheResponse.getBody()));
+ assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), javaHeaders.get(null));
+ assertEquals("ResponseBody", readAll(javaCacheResponse.getBody()));
}
@Test public void createJavaCacheResponse_httpsPost() throws Exception {
Request okRequest =
createArbitraryOkRequest().newBuilder()
.url("https://secure/request")
- .method("POST", createRequestBody("RequestBody") )
+ .post(createRequestBody("RequestBody") )
.build();
- String statusLine = "HTTP/1.1 200 Fantastic";
- Response.Body responseBody =
- createResponseBody("text/plain", "ResponseBody".getBytes(StandardCharsets.UTF_8));
+ ResponseBody responseBody = createResponseBody("ResponseBody");
Handshake handshake = Handshake.get("SecureCipher", Arrays.<Certificate>asList(SERVER_CERT),
Arrays.<Certificate>asList(LOCAL_CERT));
Response okResponse = createArbitraryOkResponse(okRequest).newBuilder()
- .statusLine(statusLine)
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Fantastic")
.addHeader("key1", "value1_1")
.addHeader("key2", "value2")
.addHeader("key1", "value1_2")
@@ -642,8 +636,8 @@
(SecureCacheResponse) JavaApiConverter.createJavaCacheResponse(okResponse);
Map<String, List<String>> javaHeaders = javaCacheResponse.getHeaders();
assertEquals(Arrays.asList("value1_1", "value1_2"), javaHeaders.get("key1"));
- assertEquals(Arrays.asList(statusLine), javaHeaders.get(null));
- assertArrayEquals(responseBody.bytes(), readAll(javaCacheResponse.getBody()));
+ assertEquals(Arrays.asList("HTTP/1.1 200 Fantastic"), javaHeaders.get(null));
+ assertEquals("ResponseBody", readAll(javaCacheResponse.getBody()));
assertEquals(handshake.cipherSuite(), javaCacheResponse.getCipherSuite());
assertEquals(handshake.localCertificates(), javaCacheResponse.getLocalCertificateChain());
assertEquals(handshake.peerCertificates(), javaCacheResponse.getServerCertificateChain());
@@ -664,7 +658,7 @@
}
@Test public void extractOkHeaders() {
- Map<String, List<String>> javaResponseHeaders = new HashMap<String, List<String>>();
+ Map<String, List<String>> javaResponseHeaders = new HashMap<>();
javaResponseHeaders.put(null, Arrays.asList("StatusLine"));
javaResponseHeaders.put("key1", Arrays.asList("value1_1", "value1_2"));
javaResponseHeaders.put("key2", Arrays.asList("value2"));
@@ -676,25 +670,24 @@
}
@Test public void extractStatusLine() {
- Map<String, List<String>> javaResponseHeaders = new HashMap<String, List<String>>();
+ Map<String, List<String>> javaResponseHeaders = new HashMap<>();
javaResponseHeaders.put(null, Arrays.asList("StatusLine"));
javaResponseHeaders.put("key1", Arrays.asList("value1_1", "value1_2"));
javaResponseHeaders.put("key2", Arrays.asList("value2"));
assertEquals("StatusLine", JavaApiConverter.extractStatusLine(javaResponseHeaders));
- assertNull(JavaApiConverter.extractStatusLine(Collections.<String, List<String>>emptyMap()));
+ assertNull(
+ JavaApiConverter.extractStatusLine(Collections.<String, List<String>>emptyMap()));
}
private URL configureServer(MockResponse mockResponse) throws Exception {
server.enqueue(mockResponse);
- server.play();
return server.getUrl("/");
}
private URL configureHttpsServer(MockResponse mockResponse) throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false /* tunnelProxy */);
+ server.get().useHttps(sslContext.getSocketFactory(), false /* tunnelProxy */);
server.enqueue(mockResponse);
- server.play();
return server.getUrl("/");
}
@@ -714,15 +707,13 @@
this.client = client;
}
- @Override
- public HttpURLConnection open(URL serverUrl) {
- return client.open(serverUrl);
+ @Override public HttpURLConnection open(URL serverUrl) {
+ return new OkUrlFactory(client).open(serverUrl);
}
}
private static class JavaHttpURLConnectionFactory implements HttpURLConnectionFactory {
- @Override
- public HttpURLConnection open(URL serverUrl) throws IOException {
+ @Override public HttpURLConnection open(URL serverUrl) throws IOException {
return (HttpURLConnection) serverUrl.openConnection();
}
}
@@ -742,20 +733,19 @@
}
private static <T> Set<T> newSet(List<T> elements) {
- return new LinkedHashSet<T>(elements);
+ return new LinkedHashSet<>(elements);
}
private static Request createArbitraryOkRequest() {
- return new Request.Builder()
- .url("http://arbitrary/url")
- .method("GET", null)
- .build();
+ return new Request.Builder().url("http://arbitrary/url").build();
}
private static Response createArbitraryOkResponse(Request request) {
return new Response.Builder()
.request(request)
- .statusLine("HTTP/1.1 200 Arbitrary")
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Arbitrary")
.build();
}
@@ -763,43 +753,35 @@
return createArbitraryOkResponse(createArbitraryOkRequest());
}
- private static Request.Body createRequestBody(String bodyText) {
- return Request.Body.create(MediaType.parse("text/plain"), bodyText);
+ private static RequestBody createRequestBody(String bodyText) {
+ return RequestBody.create(MediaType.parse("text/plain"), bodyText);
}
- private static Response.Body createResponseBody(final String contentType, final byte[] bytes) {
- return new Response.Body() {
-
- @Override
- public boolean ready() throws IOException {
- return true;
+ private static ResponseBody createResponseBody(String bodyText) {
+ final Buffer source = new Buffer().writeUtf8(bodyText);
+ final long contentLength = source.size();
+ return new ResponseBody() {
+ @Override public MediaType contentType() {
+ return MediaType.parse("text/plain; charset=utf-8");
}
- @Override
- public MediaType contentType() {
- return MediaType.parse(contentType);
+ @Override public long contentLength() {
+ return contentLength;
}
- @Override
- public long contentLength() {
- return bytes.length;
- }
-
- @Override
- public InputStream byteStream() {
- return new ByteArrayInputStream(bytes);
+ @Override public BufferedSource source() {
+ return source;
}
};
}
- private byte[] readAll(InputStream in) throws IOException {
+ private String readAll(InputStream in) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int value;
while ((value = in.read()) != -1) {
buffer.write(value);
}
in.close();
- return buffer.toByteArray();
+ return buffer.toString("UTF-8");
}
-
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheTest.java b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java
similarity index 88%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheTest.java
rename to okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java
index efec789..5fdb2fc 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ResponseCacheTest.java
+++ b/okhttp-android-support/src/test/java/com/squareup/okhttp/internal/huc/ResponseCacheTest.java
@@ -14,14 +14,19 @@
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.huc;
+import com.squareup.okhttp.AbstractResponseCache;
+import com.squareup.okhttp.Headers;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.OkUrlFactory;
+import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.http.HttpDate;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
@@ -43,27 +48,28 @@
import java.net.URLConnection;
import java.security.Principal;
import java.security.cert.Certificate;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
-import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.zip.GZIPOutputStream;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.GzipSink;
+import okio.Okio;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
@@ -75,12 +81,7 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-/**
- * Tests for interaction between OkHttp and the ResponseCache. This test is based on
- * {@link com.squareup.okhttp.internal.http.HttpResponseCacheTest}. Some tests for the
- * {@link com.squareup.okhttp.OkResponseCache} found in HttpResponseCacheTest provide
- * coverage for ResponseCache as well.
- */
+/** Tests the interaction between OkHttp and {@link ResponseCache}. */
public final class ResponseCacheTest {
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
@Override public boolean verify(String s, SSLSession sslSession) {
@@ -90,43 +91,30 @@
private static final SSLContext sslContext = SslContextBuilder.localhost();
+ @Rule public MockWebServerRule serverRule = new MockWebServerRule();
+ @Rule public MockWebServerRule server2Rule = new MockWebServerRule();
+
private OkHttpClient client;
private MockWebServer server;
private MockWebServer server2;
private ResponseCache cache;
@Before public void setUp() throws Exception {
- server = new MockWebServer();
- server.setNpnEnabled(false);
- server2 = new MockWebServer();
+ server = serverRule.get();
+ server.setProtocolNegotiationEnabled(false);
+ server2 = server2Rule.get();
client = new OkHttpClient();
cache = new InMemoryResponseCache();
- ResponseCache.setDefault(cache);
+ Internal.instance.setCache(client, new CacheAdapter(cache));
}
@After public void tearDown() throws Exception {
- server.shutdown();
- server2.shutdown();
CookieManager.setDefault(null);
}
private HttpURLConnection openConnection(URL url) {
- return client.open(url);
- }
-
- @Test public void responseCacheAccessWithOkHttpMember() throws IOException {
- ResponseCache.setDefault(null);
- client.setResponseCache(cache);
- assertSame(cache, client.getResponseCache());
- assertTrue(client.getOkResponseCache() instanceof ResponseCacheAdapter);
- }
-
- @Test public void responseCacheAccessWithGlobalDefault() throws IOException {
- ResponseCache.setDefault(cache);
- client.setResponseCache(null);
- assertNull(client.getOkResponseCache());
- assertNull(client.getResponseCache());
+ return new OkUrlFactory(client).open(url);
}
@Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
@@ -152,7 +140,6 @@
.setStatus("HTTP/1.1 200 Fantastic");
transferKind.setBody(response, "I love puppies but hate spiders", 1);
server.enqueue(response);
- server.play();
// Make sure that calling skip() doesn't omit bytes from the cache.
HttpURLConnection urlConnection = openConnection(server.getUrl("/"));
@@ -174,12 +161,36 @@
in.close();
}
+ @Test public void responseCachingWithoutBody() throws IOException {
+ MockResponse response =
+ new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setStatus("HTTP/1.1 200 Fantastic");
+ server.enqueue(response);
+
+ // Make sure that calling skip() doesn't omit bytes from the cache.
+ HttpURLConnection urlConnection = openConnection(server.getUrl("/"));
+ assertEquals(200, urlConnection.getResponseCode());
+ assertEquals("Fantastic", urlConnection.getResponseMessage());
+ assertTrue(urlConnection.getDoInput());
+ InputStream is = urlConnection.getInputStream();
+ assertEquals(-1, is.read());
+ is.close();
+
+ urlConnection = openConnection(server.getUrl("/")); // cached!
+ assertTrue(urlConnection.getDoInput());
+ InputStream cachedIs = urlConnection.getInputStream();
+ assertEquals(-1, cachedIs.read());
+ cachedIs.close();
+ assertEquals(200, urlConnection.getResponseCode());
+ assertEquals("Fantastic", urlConnection.getResponseMessage());
+ }
+
@Test public void secureResponseCaching() throws IOException {
server.useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
- server.play();
HttpsURLConnection c1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
c1.setSSLSocketFactory(sslContext.getSocketFactory());
@@ -209,9 +220,9 @@
server.useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
- client.setResponseCache(new InsecureResponseCache(new InMemoryResponseCache()));
+ Internal.instance.setCache(client,
+ new CacheAdapter(new InsecureResponseCache(new InMemoryResponseCache())));
HttpsURLConnection connection1 = (HttpsURLConnection) openConnection(server.getUrl("/"));
connection1.setSSLSocketFactory(sslContext.getSocketFactory());
@@ -234,7 +245,6 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
HttpURLConnection connection = openConnection(server.getUrl("/"));
assertEquals("ABC", readAscii(connection));
@@ -248,7 +258,6 @@
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: /foo"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo"))));
RecordedRequest request1 = server.takeRequest();
@@ -277,7 +286,6 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
@@ -308,13 +316,11 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server2.enqueue(new MockResponse().setBody("DEF"));
- server2.play();
server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: " + server2.getUrl("/")));
- server.play();
client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
@@ -329,23 +335,16 @@
@Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException {
server.enqueue(new MockResponse().setBody("ABC"));
- server.play();
final AtomicReference<Map<String, List<String>>> requestHeadersRef =
new AtomicReference<Map<String, List<String>>>();
- client.setResponseCache(new ResponseCache() {
- @Override
- public CacheResponse get(URI uri, String requestMethod,
+ Internal.instance.setCache(client, new CacheAdapter(new AbstractResponseCache() {
+ @Override public CacheResponse get(URI uri, String requestMethod,
Map<String, List<String>> requestHeaders) throws IOException {
requestHeadersRef.set(requestHeaders);
return null;
}
-
- @Override
- public CacheRequest put(URI uri, URLConnection conn) throws IOException {
- return null;
- }
- });
+ }));
URL url = server.getUrl("/");
URLConnection urlConnection = openConnection(url);
@@ -373,7 +372,6 @@
transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
server.enqueue(truncateViolently(response, 16));
server.enqueue(new MockResponse().setBody("Request #2"));
- server.play();
BufferedReader reader = new BufferedReader(
new InputStreamReader(openConnection(server.getUrl("/")).getInputStream()));
@@ -382,7 +380,6 @@
reader.readLine();
fail("This implementation silently ignored a truncated HTTP body.");
} catch (IOException expected) {
- expected.printStackTrace();
} finally {
reader.close();
}
@@ -409,7 +406,6 @@
transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
server.enqueue(response);
server.enqueue(new MockResponse().setBody("Request #2"));
- server.play();
URLConnection connection = openConnection(server.getUrl("/"));
InputStream in = connection.getInputStream();
@@ -434,7 +430,6 @@
new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
.setBody("A"));
- server.play();
URL url = server.getUrl("/");
assertEquals("A", readAscii(openConnection(url)));
@@ -452,8 +447,7 @@
RecordedRequest conditionalRequest = assertConditionallyCached(
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
@@ -464,7 +458,6 @@
server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
.addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
.setBody("A"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -479,7 +472,6 @@
.addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/?foo=bar");
assertEquals("A", readAscii(openConnection(url)));
@@ -491,8 +483,7 @@
RecordedRequest conditionalRequest = assertConditionallyCached(
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception {
@@ -515,8 +506,7 @@
new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Cache-Control: max-age=60"));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
@@ -582,16 +572,17 @@
server.enqueue(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("X-Response-ID: 1"));
server.enqueue(new MockResponse().addHeader("X-Response-ID: 2"));
- server.play();
URL url = server.getUrl("/");
HttpURLConnection request1 = openConnection(url);
request1.setRequestMethod(requestMethod);
addRequestBodyIfNecessary(requestMethod, request1);
+ request1.getInputStream().close();
assertEquals("1", request1.getHeaderField("X-Response-ID"));
URLConnection request2 = openConnection(url);
+ request2.getInputStream().close();
if (expectCached) {
assertEquals("1", request2.getHeaderField("X-Response-ID"));
} else {
@@ -600,10 +591,9 @@
}
/**
- * Equivalent to {@link HttpResponseCacheTest#postInvalidatesCacheWithUncacheableResponse()} but
- * demonstrating that {@link ResponseCache} provides no mechanism for cache invalidation as the
- * result of locally-made requests. In reality invalidation could take place from other clients at
- * any time.
+ * Equivalent to {@code CacheTest.postInvalidatesCacheWithUncacheableResponse()} but demonstrating
+ * that {@link ResponseCache} provides no mechanism for cache invalidation as the result of
+ * locally-made requests. In reality invalidation could take place from other clients at any time.
*/
@Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception {
// 1. seed the cache
@@ -612,7 +602,6 @@
server.enqueue(
new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B").setResponseCode(500));
- server.play();
URL url = server.getUrl("/");
@@ -629,7 +618,7 @@
@Test public void etag() throws Exception {
RecordedRequest conditionalRequest =
assertConditionallyCached(new MockResponse().addHeader("ETag: v1"));
- assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1"));
+ assertEquals("v1", conditionalRequest.getHeader("If-None-Match"));
}
@Test public void etagAndExpirationDateInThePast() throws Exception {
@@ -638,9 +627,8 @@
new MockResponse().addHeader("ETag: v1")
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-None-Match: v1"));
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals("v1", conditionalRequest.getHeader("If-None-Match"));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void etagAndExpirationDateInTheFuture() throws Exception {
@@ -659,8 +647,7 @@
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Cache-Control: no-cache"));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void pragmaNoCache() throws Exception {
@@ -673,8 +660,7 @@
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Pragma: no-cache"));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void cacheControlNoStore() throws Exception {
@@ -695,7 +681,6 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Content-Range: bytes 1000-1001/2000"));
server.enqueue(new MockResponse().setBody("BB"));
- server.play();
URL url = server.getUrl("/");
@@ -712,7 +697,6 @@
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B")
.addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
- server.play();
URL url = server.getUrl("/");
@@ -734,12 +718,10 @@
private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
server.enqueue(
- response.setBody(gzip("ABCABCABC".getBytes("UTF-8"))).addHeader("Content-Encoding: gzip"));
+ response.setBody(gzip("ABCABCABC")).addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
-
// At least three request/response pairs are required because after the first request is cached
// a different execution path might be taken. Thus modifications to the cache applied during
// the second request might not be visible until another request is performed.
@@ -750,7 +732,7 @@
@Test public void notModifiedSpecifiesEncoding() throws Exception {
server.enqueue(new MockResponse()
- .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
+ .setBody(gzip("ABCABCABC"))
.addHeader("Content-Encoding: gzip")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
@@ -760,7 +742,6 @@
server.enqueue(new MockResponse()
.setBody("DEFDEFDEF"));
- server.play();
assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
assertEquals("DEFDEFDEF", readAscii(openConnection(server.getUrl("/"))));
@@ -779,7 +760,6 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -793,7 +773,6 @@
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -807,7 +786,6 @@
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -823,7 +801,6 @@
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -833,7 +810,6 @@
@Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException {
// (no responses enqueued)
- server.play();
HttpURLConnection connection = openConnection(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
@@ -844,7 +820,6 @@
server.enqueue(new MockResponse().setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -856,7 +831,6 @@
server.enqueue(new MockResponse().setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
HttpURLConnection connection = openConnection(server.getUrl("/"));
@@ -866,7 +840,6 @@
@Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
HttpURLConnection connection = openConnection(server.getUrl("/"));
@@ -881,7 +854,6 @@
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
assertEquals("A", readAscii(openConnection(url)));
@@ -897,7 +869,6 @@
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
assertEquals("A", readAscii(openConnection(url)));
@@ -912,9 +883,8 @@
String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
RecordedRequest request =
assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate);
- List<String> headers = request.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate));
- assertFalse(headers.contains("If-None-Match: v3"));
+ assertEquals(ifModifiedSinceDate, request.getHeader("If-Modified-Since"));
+ assertNull(request.getHeader("If-None-Match"));
}
@Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
@@ -923,16 +893,14 @@
.addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
.addHeader("Cache-Control: max-age=0");
RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1");
- List<String> headers = request.getHeaders();
- assertTrue(headers.contains("If-None-Match: v1"));
- assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals("v1", request.getHeader("If-None-Match"));
+ assertNull(request.getHeader("If-Modified-Since"));
}
private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
String conditionValue) throws Exception {
server.enqueue(seed.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URL url = server.getUrl("/");
assertEquals("A", readAscii(openConnection(url)));
@@ -949,19 +917,17 @@
@Test public void setIfModifiedSince() throws Exception {
Date since = new Date();
server.enqueue(new MockResponse().setBody("A"));
- server.play();
URL url = server.getUrl("/");
URLConnection connection = openConnection(url);
connection.setIfModifiedSince(since.getTime());
assertEquals("A", readAscii(connection));
RecordedRequest request = server.takeRequest();
- assertTrue(request.getHeaders().contains("If-Modified-Since: " + HttpDate.format(since)));
+ assertEquals(HttpDate.format(since), request.getHeader("If-Modified-Since"));
}
@Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
HttpURLConnection connection = openConnection(server.getUrl("/"));
String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS);
@@ -970,39 +936,9 @@
assertEquals("", readAscii(connection));
}
- @Test public void authorizationRequestHeaderPreventsCaching() throws Exception {
- server.enqueue(
- new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES))
- .addHeader("Cache-Control: max-age=60")
- .setBody("A"));
+ @Test public void authorizationRequestFullyCached() throws Exception {
+ server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
-
- URL url = server.getUrl("/");
- URLConnection connection = openConnection(url);
- connection.addRequestProperty("Authorization", "password");
- assertEquals("A", readAscii(connection));
- assertEquals("B", readAscii(openConnection(url)));
- }
-
- @Test public void authorizationResponseCachedWithSMaxAge() throws Exception {
- assertAuthorizationRequestFullyCached(
- new MockResponse().addHeader("Cache-Control: s-maxage=60"));
- }
-
- @Test public void authorizationResponseCachedWithPublic() throws Exception {
- assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public"));
- }
-
- @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception {
- assertAuthorizationRequestFullyCached(
- new MockResponse().addHeader("Cache-Control: must-revalidate"));
- }
-
- public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception {
- server.enqueue(response.addHeader("Cache-Control: max-age=60").setBody("A"));
- server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
URLConnection connection = openConnection(url);
@@ -1016,7 +952,6 @@
.addHeader("Content-Location: /bar")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/foo"))));
assertEquals("B", readAscii(openConnection(server.getUrl("/bar"))));
@@ -1026,7 +961,6 @@
server.enqueue(
new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URLConnection connection = openConnection(server.getUrl("/"));
connection.setUseCaches(false);
@@ -1038,7 +972,6 @@
server.enqueue(
new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
@@ -1068,7 +1001,6 @@
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
@@ -1090,7 +1022,6 @@
server.enqueue(
new MockResponse().addHeader("Cache-Control: max-age=60")
.addHeader(InMemoryResponseCache.CACHE_VARIANT_HEADER, "B").setBody("B"));
- server.play();
URL url = server.getUrl("/");
URLConnection connection1 = openConnection(url);
@@ -1121,7 +1052,6 @@
server.enqueue(new MockResponse().addHeader(
"Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
CookieManager cookieManager = new CookieManager();
CookieManager.setDefault(cookieManager);
@@ -1140,7 +1070,6 @@
.setBody("A"));
server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD, PUT")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URLConnection connection1 = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
@@ -1158,7 +1087,6 @@
.setBody("A"));
server.enqueue(new MockResponse().addHeader("Transfer-Encoding: none")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URLConnection connection1 = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
@@ -1175,7 +1103,6 @@
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URLConnection connection1 = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
@@ -1192,7 +1119,6 @@
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URLConnection connection1 = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
@@ -1220,9 +1146,8 @@
}
/**
- * Equivalent to {@link HttpResponseCacheTest#conditionalHitUpdatesCache()}, except a Java
- * standard cache has no means to update the headers for an existing entry so the behavior is
- * different.
+ * Equivalent to {@code CacheTest.conditionalHitUpdatesCache()}, except a Java standard cache has
+ * no means to update the headers for an existing entry so the behavior is different.
*/
@Test public void conditionalHitDoesNotUpdateCache() throws Exception {
// A response that is cacheable, but with a short life.
@@ -1238,7 +1163,6 @@
server.enqueue(new MockResponse().setBody("B").addHeader("Cache-Control: max-age=30"));
// A response that should never be requested.
server.enqueue(new MockResponse().setBody("C"));
- server.play();
// cache miss; seed the cache with an entry that will require a network hit to be sure it is
// still valid
@@ -1267,15 +1191,11 @@
server.enqueue(new MockResponse().setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
URLConnection connection = openConnection(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertEquals("A", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.CACHE + " 200", source);
}
@Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
@@ -1285,14 +1205,10 @@
server.enqueue(new MockResponse().setBody("B")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
HttpURLConnection connection = openConnection(server.getUrl("/"));
assertEquals("B", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source);
}
@Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
@@ -1300,38 +1216,95 @@
.addHeader("Cache-Control: max-age=0")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setResponseCode(304));
- server.play();
assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
HttpURLConnection connection = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source);
}
@Test public void responseSourceHeaderFetched() throws IOException {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
URLConnection connection = openConnection(server.getUrl("/"));
assertEquals("A", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.NETWORK + " 200", source);
}
@Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
+ Headers.Builder headers = new Headers.Builder()
+ .add("Cache-Control: max-age=120");
+ Internal.instance.addLenient(headers, ": A");
server.enqueue(new MockResponse()
- .addHeader("Cache-Control: max-age=120")
- .addHeader(": A")
+ .setHeaders(headers.build())
.setBody("body"));
- server.play();
+
HttpURLConnection connection = openConnection(server.getUrl("/"));
assertEquals("A", connection.getHeaderField(""));
}
/**
+ * Test that we can interrogate the response when the cache is being
+ * populated. http://code.google.com/p/android/issues/detail?id=7787
+ */
+ @Test public void responseCacheCallbackApis() throws Exception {
+ final String body = "ABCDE";
+ final AtomicInteger cacheCount = new AtomicInteger();
+
+ server.enqueue(new MockResponse()
+ .setStatus("HTTP/1.1 200 Fantastic")
+ .addHeader("Content-Type: text/plain")
+ .addHeader("fgh: ijk")
+ .setBody(body));
+
+ Internal.instance.setCache(client, new CacheAdapter(new AbstractResponseCache() {
+ @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
+ assertEquals(server.getUrl("/"), uri.toURL());
+ assertEquals(200, httpURLConnection.getResponseCode());
+ try {
+ httpURLConnection.getInputStream();
+ fail();
+ } catch (UnsupportedOperationException expected) {
+ }
+ assertEquals("5", connection.getHeaderField("Content-Length"));
+ assertEquals("text/plain", connection.getHeaderField("Content-Type"));
+ assertEquals("ijk", connection.getHeaderField("fgh"));
+ cacheCount.incrementAndGet();
+ return null;
+ }
+ }));
+
+ URL url = server.getUrl("/");
+ HttpURLConnection connection = openConnection(url);
+ assertEquals(body, readAscii(connection));
+ assertEquals(1, cacheCount.get());
+ }
+
+ /** Don't explode if the cache returns a null body. http://b/3373699 */
+ @Test public void responseCacheReturnsNullOutputStream() throws Exception {
+ final AtomicBoolean aborted = new AtomicBoolean();
+ Internal.instance.setCache(client, new CacheAdapter(new AbstractResponseCache() {
+ @Override public CacheRequest put(URI uri, URLConnection connection) {
+ return new CacheRequest() {
+ @Override public void abort() {
+ aborted.set(true);
+ }
+
+ @Override public OutputStream getBody() throws IOException {
+ return null;
+ }
+ };
+ }
+ }));
+
+ server.enqueue(new MockResponse().setBody("abcdef"));
+
+ HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("abc", readAscii(connection, 3));
+ connection.getInputStream().close();
+ assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
+ }
+
+ /**
* @param delta the offset from the current date to use. Negative
* values yield dates in the past; positive values yield dates in the
* future.
@@ -1353,7 +1326,6 @@
private void assertNotCached(MockResponse response) throws Exception {
server.enqueue(response.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
assertEquals("A", readAscii(openConnection(url)));
@@ -1370,8 +1342,6 @@
server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK"));
server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C"));
- server.play();
-
URL valid = server.getUrl("/valid");
HttpURLConnection connection1 = openConnection(valid);
assertEquals("A", readAscii(connection1));
@@ -1399,7 +1369,6 @@
private void assertFullyCached(MockResponse response) throws Exception {
server.enqueue(response.setBody("A"));
server.enqueue(response.setBody("B"));
- server.play();
URL url = server.getUrl("/");
assertEquals("A", readAscii(openConnection(url)));
@@ -1413,10 +1382,11 @@
*/
private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
response.setSocketPolicy(DISCONNECT_AT_END);
- List<String> headers = new ArrayList<String>(response.getHeaders());
- response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
- response.getHeaders().clear();
- response.getHeaders().addAll(headers);
+ Headers headers = response.getHeaders();
+ Buffer truncatedBody = new Buffer();
+ truncatedBody.write(response.getBody(), numBytesToKeep);
+ response.setBody(truncatedBody);
+ response.setHeaders(headers);
return response;
}
@@ -1459,39 +1429,32 @@
}
assertEquals(504, connection.getResponseCode());
assertEquals(-1, connection.getErrorStream().read());
- assertEquals(ResponseSource.NONE + " 504",
- connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
}
enum TransferKind {
CHUNKED() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize)
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize)
throws IOException {
response.setChunkedBody(content, chunkSize);
}
},
FIXED_LENGTH() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
}
},
END_OF_STREAM() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
response.setSocketPolicy(DISCONNECT_AT_END);
- for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
- if (h.next().startsWith("Content-Length:")) {
- h.remove();
- break;
- }
- }
+ response.removeHeader("Content-Length");
}
};
- abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException;
+ abstract void setBody(MockResponse response, Buffer content, int chunkSize) throws IOException;
void setBody(MockResponse response, String content, int chunkSize) throws IOException {
- setBody(response, content.getBytes("UTF-8"), chunkSize);
+ setBody(response, new Buffer().writeUtf8(content), chunkSize);
}
}
@@ -1500,12 +1463,12 @@
}
/** Returns a gzipped copy of {@code bytes}. */
- public byte[] gzip(byte[] bytes) throws IOException {
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
- gzippedOut.write(bytes);
- gzippedOut.close();
- return bytesOut.toByteArray();
+ public Buffer gzip(String data) throws IOException {
+ Buffer result = new Buffer();
+ BufferedSink sink = Okio.buffer(new GzipSink(result));
+ sink.writeUtf8(data);
+ sink.close();
+ return result;
}
private static class InsecureResponseCache extends ResponseCache {
diff --git a/okhttp-apache/pom.xml b/okhttp-apache/pom.xml
index 14eb349..4031304 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.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>okhttp-apache</artifactId>
@@ -19,12 +19,6 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>com.squareup.okhttp</groupId>
- <artifactId>mockwebserver</artifactId>
- <version>${project.version}</version>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>provided</scope>
@@ -35,5 +29,27 @@
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <links>
+ <link>http://square.github.io/okhttp/javadoc/</link>
+ <link>http://hc.apache.org/httpcomponents-client-4.3.x/httpclient/apidocs/</link>
+ <link>https://hc.apache.org/httpcomponents-core-4.3.x/httpcore/apidocs/</link>
+ </links>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/okhttp-apache/src/main/java/com/squareup/okhttp/apache/HttpEntityBody.java b/okhttp-apache/src/main/java/com/squareup/okhttp/apache/HttpEntityBody.java
new file mode 100644
index 0000000..fd7884c
--- /dev/null
+++ b/okhttp-apache/src/main/java/com/squareup/okhttp/apache/HttpEntityBody.java
@@ -0,0 +1,41 @@
+package com.squareup.okhttp.apache;
+
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
+import java.io.IOException;
+import okio.BufferedSink;
+import org.apache.http.HttpEntity;
+
+/** Adapts an {@link HttpEntity} to OkHttp's {@link RequestBody}. */
+final class HttpEntityBody extends RequestBody {
+ private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.parse("application/octet-stream");
+
+ private final HttpEntity entity;
+ private final MediaType mediaType;
+
+ HttpEntityBody(HttpEntity entity, String contentTypeHeader) {
+ this.entity = entity;
+
+ if (contentTypeHeader != null) {
+ mediaType = MediaType.parse(contentTypeHeader);
+ } else if (entity.getContentType() != null) {
+ mediaType = MediaType.parse(entity.getContentType().getValue());
+ } else {
+ // Apache is forgiving and lets you skip specifying a content type with an entity. OkHttp is
+ // not forgiving so we fall back to a generic type if it's missing.
+ mediaType = DEFAULT_MEDIA_TYPE;
+ }
+ }
+
+ @Override public long contentLength() {
+ return entity.getContentLength();
+ }
+
+ @Override public MediaType contentType() {
+ return mediaType;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ entity.writeTo(sink.outputStream());
+ }
+}
diff --git a/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java b/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java
index 2c40f9a..602a2c8 100644
--- a/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java
+++ b/okhttp-apache/src/main/java/com/squareup/okhttp/apache/OkApacheClient.java
@@ -1,13 +1,15 @@
// Copyright 2013 Square, Inc.
package com.squareup.okhttp.apache;
+import com.squareup.okhttp.Headers;
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 java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
-import java.net.URL;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
@@ -21,7 +23,6 @@
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRouteParams;
import org.apache.http.entity.InputStreamEntity;
-import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.params.AbstractHttpParams;
import org.apache.http.params.HttpParams;
@@ -37,8 +38,65 @@
* API. This includes the keep-alive strategy, cookie store, credentials provider, route planner
* and others.
*/
-public class OkApacheClient implements HttpClient {
- protected final OkHttpClient client;
+public final class OkApacheClient implements HttpClient {
+ private static Request transformRequest(HttpRequest request) {
+ Request.Builder builder = new Request.Builder();
+
+ RequestLine requestLine = request.getRequestLine();
+ String method = requestLine.getMethod();
+ builder.url(requestLine.getUri());
+
+ String contentType = null;
+ for (Header header : request.getAllHeaders()) {
+ String name = header.getName();
+ if ("Content-Type".equals(name)) {
+ contentType = header.getValue();
+ } else {
+ builder.header(name, header.getValue());
+ }
+ }
+
+ RequestBody body = null;
+ if (request instanceof HttpEntityEnclosingRequest) {
+ HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+ if (entity != null) {
+ // Wrap the entity in a custom Body which takes care of the content, length, and type.
+ body = new HttpEntityBody(entity, contentType);
+
+ Header encoding = entity.getContentEncoding();
+ if (encoding != null) {
+ builder.header(encoding.getName(), encoding.getValue());
+ }
+ }
+ }
+ builder.method(method, body);
+
+ return builder.build();
+ }
+
+ private static HttpResponse transformResponse(Response response) throws IOException {
+ int code = response.code();
+ String message = response.message();
+ BasicHttpResponse httpResponse = new BasicHttpResponse(HTTP_1_1, code, message);
+
+ ResponseBody body = response.body();
+ InputStreamEntity entity = new InputStreamEntity(body.byteStream(), body.contentLength());
+ httpResponse.setEntity(entity);
+
+ Headers headers = response.headers();
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ String name = headers.name(i);
+ String value = headers.value(i);
+ httpResponse.addHeader(name, value);
+ if ("Content-Type".equalsIgnoreCase(name)) {
+ entity.setContentType(value);
+ } else if ("Content-Encoding".equalsIgnoreCase(name)) {
+ entity.setContentEncoding(value);
+ }
+ }
+
+ return httpResponse;
+ }
private final HttpParams params = new AbstractHttpParams() {
@Override public Object getParameter(String name) {
@@ -75,6 +133,8 @@
}
};
+ private final OkHttpClient client;
+
public OkApacheClient() {
this(new OkHttpClient());
}
@@ -83,14 +143,6 @@
this.client = client;
}
- /**
- * Returns a new HttpURLConnection customized for this application. Subclasses should override
- * this to customize the connection.
- */
- protected HttpURLConnection openConnection(URL url) {
- return client.open(url);
- }
-
@Override public HttpParams getParams() {
return params;
}
@@ -114,66 +166,9 @@
@Override public HttpResponse execute(HttpHost host, HttpRequest request, HttpContext context)
throws IOException {
- // Prepare the request headers.
- RequestLine requestLine = request.getRequestLine();
- URL url = new URL(requestLine.getUri());
- HttpURLConnection connection = openConnection(url);
- connection.setRequestMethod(requestLine.getMethod());
- for (Header header : request.getAllHeaders()) {
- connection.addRequestProperty(header.getName(), header.getValue());
- }
-
- // Stream the request body.
- if (request instanceof HttpEntityEnclosingRequest) {
- HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
- if (entity != null) {
- connection.setDoOutput(true);
- Header type = entity.getContentType();
- if (type != null) {
- connection.addRequestProperty(type.getName(), type.getValue());
- }
- Header encoding = entity.getContentEncoding();
- if (encoding != null) {
- connection.addRequestProperty(encoding.getName(), encoding.getValue());
- }
- if (entity.isChunked() || entity.getContentLength() < 0) {
- connection.setChunkedStreamingMode(0);
- } else if (entity.getContentLength() <= 8192) {
- // Buffer short, fixed-length request bodies. This costs memory, but permits the request
- // to be transparently retried if there is a connection failure.
- connection.addRequestProperty("Content-Length", Long.toString(entity.getContentLength()));
- } else {
- connection.setFixedLengthStreamingMode((int) entity.getContentLength());
- }
- entity.writeTo(connection.getOutputStream());
- }
- }
-
- // Read the response headers.
- int responseCode = connection.getResponseCode();
- String message = connection.getResponseMessage();
- BasicHttpResponse response = new BasicHttpResponse(HTTP_1_1, responseCode, message);
- // Get the response body ready to stream.
- InputStream responseBody =
- responseCode < HttpURLConnection.HTTP_BAD_REQUEST ? connection.getInputStream()
- : connection.getErrorStream();
- InputStreamEntity entity = new InputStreamEntity(responseBody, connection.getContentLength());
- for (int i = 0; true; i++) {
- String name = connection.getHeaderFieldKey(i);
- if (name == null) {
- break;
- }
- BasicHeader header = new BasicHeader(name, connection.getHeaderField(i));
- response.addHeader(header);
- if (name.equalsIgnoreCase("Content-Type")) {
- entity.setContentType(header);
- } else if (name.equalsIgnoreCase("Content-Encoding")) {
- entity.setContentEncoding(header);
- }
- }
- response.setEntity(entity);
-
- return response;
+ Request okRequest = transformRequest(request);
+ Response okResponse = client.newCall(okRequest).execute();
+ return transformResponse(okResponse);
}
@Override public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> handler)
diff --git a/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java b/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java
index 766e69c..ca47c01 100644
--- a/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java
+++ b/okhttp-apache/src/test/java/com/squareup/okhttp/apache/OkApacheClientTest.java
@@ -4,29 +4,29 @@
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStreamWriter;
-import java.nio.charset.Charset;
-import java.util.Arrays;
import java.util.zip.GZIPInputStream;
-import java.util.zip.GZIPOutputStream;
+import okio.Buffer;
+import okio.GzipSink;
+import okio.Okio;
import org.apache.http.Header;
+import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
+import org.apache.http.entity.StringEntity;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
+import static com.squareup.okhttp.internal.Util.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
public class OkApacheClientTest {
private MockWebServer server;
@@ -35,7 +35,7 @@
@Before public void setUp() throws IOException {
client = new OkApacheClient();
server = new MockWebServer();
- server.play();
+ server.start();
}
@After public void tearDown() throws IOException {
@@ -57,7 +57,7 @@
HttpGet request = new HttpGet(server.getUrl("/").toURI());
HttpResponse response = client.execute(request);
- String actual = EntityUtils.toString(response.getEntity());
+ String actual = EntityUtils.toString(response.getEntity(), UTF_8);
assertEquals("Hello, Redirect!", actual);
}
@@ -87,23 +87,16 @@
assertEquals("Baz", headers2[1].getValue());
}
- @Test public void noEntity() throws Exception {
- server.enqueue(new MockResponse());
-
- HttpPost post = new HttpPost(server.getUrl("/").toURI());
- client.execute(post);
- }
-
@Test public void postByteEntity() throws Exception {
server.enqueue(new MockResponse());
final HttpPost post = new HttpPost(server.getUrl("/").toURI());
- byte[] body = "Hello, world!".getBytes("UTF-8");
+ byte[] body = "Hello, world!".getBytes(UTF_8);
post.setEntity(new ByteArrayEntity(body));
client.execute(post);
RecordedRequest request = server.takeRequest();
- assertTrue(Arrays.equals(body, request.getBody()));
+ assertEquals("Hello, world!", request.getBody().readUtf8());
assertEquals(request.getHeader("Content-Length"), "13");
}
@@ -111,15 +104,28 @@
server.enqueue(new MockResponse());
final HttpPost post = new HttpPost(server.getUrl("/").toURI());
- byte[] body = "Hello, world!".getBytes("UTF-8");
+ byte[] body = "Hello, world!".getBytes(UTF_8);
post.setEntity(new InputStreamEntity(new ByteArrayInputStream(body), body.length));
client.execute(post);
RecordedRequest request = server.takeRequest();
- assertTrue(Arrays.equals(body, request.getBody()));
+ assertEquals("Hello, world!", request.getBody().readUtf8());
assertEquals(request.getHeader("Content-Length"), "13");
}
+ @Test public void postOverrideContentType() throws Exception {
+ server.enqueue(new MockResponse());
+
+ HttpPost httpPost = new HttpPost();
+ httpPost.setURI(server.getUrl("/").toURI());
+ httpPost.addHeader("Content-Type", "application/xml");
+ httpPost.setEntity(new StringEntity("<yo/>"));
+ client.execute(httpPost);
+
+ RecordedRequest request = server.takeRequest();
+ assertEquals(request.getHeader("Content-Type"), "application/xml");
+ }
+
@Test public void contentType() throws Exception {
server.enqueue(new MockResponse().setBody("<html><body><h1>Hello, World!</h1></body></html>")
.setHeader("Content-Type", "text/html"));
@@ -152,102 +158,93 @@
@Test public void contentEncoding() throws Exception {
String text = "{\"Message\": { \"text\": \"Hello, World!\" } }";
- ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream();
- OutputStreamWriter body = new OutputStreamWriter(new GZIPOutputStream(bodyBytes),
- Charset.forName("UTF-8"));
- body.write(text);
- body.close();
- server.enqueue(new MockResponse().setBody(bodyBytes.toByteArray())
+ server.enqueue(new MockResponse().setBody(gzip(text))
.setHeader("Content-Encoding", "gzip"));
- byte[] tmp = new byte[32];
+ HttpGet request = new HttpGet(server.getUrl("/").toURI());
+ request.setHeader("Accept-encoding", "gzip"); // Not transparent gzip.
+ HttpResponse response = client.execute(request);
+ HttpEntity entity = response.getEntity();
- HttpGet request1 = new HttpGet(server.getUrl("/").toURI());
- request1.setHeader("Accept-encoding", "gzip"); // not transparent gzip
- HttpResponse response1 = client.execute(request1);
- Header[] headers1 = response1.getHeaders("Content-Encoding");
- assertEquals(1, headers1.length);
- assertEquals("gzip", headers1[0].getValue());
- assertNotNull(response1.getEntity().getContentEncoding());
- assertEquals("gzip", response1.getEntity().getContentEncoding().getValue());
- InputStream content = new GZIPInputStream(response1.getEntity().getContent());
- ByteArrayOutputStream rspBodyBytes = new ByteArrayOutputStream();
- for (int len = content.read(tmp); len >= 0; len = content.read(tmp)) {
- rspBodyBytes.write(tmp, 0, len);
- }
- String decodedContent = rspBodyBytes.toString("UTF-8");
- assertEquals(text, decodedContent);
+ Header[] encodingHeaders = response.getHeaders("Content-Encoding");
+ assertEquals(1, encodingHeaders.length);
+ assertEquals("gzip", encodingHeaders[0].getValue());
+ assertNotNull(entity.getContentEncoding());
+ assertEquals("gzip", entity.getContentEncoding().getValue());
+
+ assertEquals(text, gunzip(entity));
}
@Test public void jsonGzipResponse() throws Exception {
String text = "{\"Message\": { \"text\": \"Hello, World!\" } }";
- ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream();
- OutputStreamWriter body = new OutputStreamWriter(new GZIPOutputStream(bodyBytes),
- Charset.forName("UTF-8"));
- body.write(text);
- body.close();
- server.enqueue(new MockResponse().setBody(bodyBytes.toByteArray())
+ server.enqueue(new MockResponse().setBody(gzip(text))
.setHeader("Content-Encoding", "gzip")
.setHeader("Content-Type", "application/json"));
- byte[] tmp = new byte[32];
-
HttpGet request1 = new HttpGet(server.getUrl("/").toURI());
- request1.setHeader("Accept-encoding", "gzip"); // not transparent gzip
- HttpResponse response1 = client.execute(request1);
- Header[] headers1a = response1.getHeaders("Content-Encoding");
- assertEquals(1, headers1a.length);
- assertEquals("gzip", headers1a[0].getValue());
- assertNotNull(response1.getEntity().getContentEncoding());
- assertEquals("gzip", response1.getEntity().getContentEncoding().getValue());
- Header[] headers1b = response1.getHeaders("Content-Type");
- assertEquals(1, headers1b.length);
- assertEquals("application/json", headers1b[0].getValue());
- assertNotNull(response1.getEntity().getContentType());
- assertEquals("application/json", response1.getEntity().getContentType().getValue());
- InputStream content = new GZIPInputStream(response1.getEntity().getContent());
- ByteArrayOutputStream rspBodyBytes = new ByteArrayOutputStream();
- for (int len = content.read(tmp); len >= 0; len = content.read(tmp)) {
- rspBodyBytes.write(tmp, 0, len);
- }
- String decodedContent = rspBodyBytes.toString("UTF-8");
- assertEquals(text, decodedContent);
+ request1.setHeader("Accept-encoding", "gzip"); // Not transparent gzip.
+
+ HttpResponse response = client.execute(request1);
+ HttpEntity entity = response.getEntity();
+
+ Header[] encodingHeaders = response.getHeaders("Content-Encoding");
+ assertEquals(1, encodingHeaders.length);
+ assertEquals("gzip", encodingHeaders[0].getValue());
+ assertNotNull(entity.getContentEncoding());
+ assertEquals("gzip", entity.getContentEncoding().getValue());
+
+ Header[] typeHeaders = response.getHeaders("Content-Type");
+ assertEquals(1, typeHeaders.length);
+ assertEquals("application/json", typeHeaders[0].getValue());
+ assertNotNull(entity.getContentType());
+ assertEquals("application/json", entity.getContentType().getValue());
+
+ assertEquals(text, gunzip(entity));
}
@Test public void jsonTransparentGzipResponse() throws Exception {
String text = "{\"Message\": { \"text\": \"Hello, World!\" } }";
- ByteArrayOutputStream bodyBytes = new ByteArrayOutputStream();
- OutputStreamWriter body = new OutputStreamWriter(new GZIPOutputStream(bodyBytes),
- Charset.forName("UTF-8"));
- body.write(text);
- body.close();
- server.enqueue(new MockResponse().setBody(bodyBytes.toByteArray())
+ server.enqueue(new MockResponse().setBody(gzip(text))
.setHeader("Content-Encoding", "gzip")
.setHeader("Content-Type", "application/json"));
- byte[] tmp = new byte[32];
+ HttpGet request = new HttpGet(server.getUrl("/").toURI());
+ HttpResponse response = client.execute(request);
+ HttpEntity entity = response.getEntity();
- HttpGet request1 = new HttpGet(server.getUrl("/").toURI());
- // expecting transparent gzip response by not adding header "Accept-encoding: gzip"
- HttpResponse response1 = client.execute(request1);
- Header[] headers1a = response1.getHeaders("Content-Encoding");
- assertEquals(0, headers1a.length);
- assertNull(response1.getEntity().getContentEncoding());
- // content length should also be absent
- Header[] headers1b = response1.getHeaders("Content-Length");
- assertEquals(0, headers1b.length);
- assertTrue(response1.getEntity().getContentLength() < 0);
- Header[] headers1c = response1.getHeaders("Content-Type");
- assertEquals(1, headers1c.length);
- assertEquals("application/json", headers1c[0].getValue());
- assertNotNull(response1.getEntity().getContentType());
- assertEquals("application/json", response1.getEntity().getContentType().getValue());
- InputStream content = response1.getEntity().getContent();
- ByteArrayOutputStream rspBodyBytes = new ByteArrayOutputStream();
- for (int len = content.read(tmp); len >= 0; len = content.read(tmp)) {
- rspBodyBytes.write(tmp, 0, len);
+ // Expecting transparent gzip response by not adding header "Accept-encoding: gzip"
+ Header[] encodingHeaders = response.getHeaders("Content-Encoding");
+ assertEquals(0, encodingHeaders.length);
+ assertNull(entity.getContentEncoding());
+
+ // Content length should be absent.
+ Header[] lengthHeaders = response.getHeaders("Content-Length");
+ assertEquals(0, lengthHeaders.length);
+ assertEquals(-1, entity.getContentLength());
+
+ Header[] typeHeaders = response.getHeaders("Content-Type");
+ assertEquals(1, typeHeaders.length);
+ assertEquals("application/json", typeHeaders[0].getValue());
+ assertNotNull(entity.getContentType());
+ assertEquals("application/json", entity.getContentType().getValue());
+
+ assertEquals(text, EntityUtils.toString(entity, UTF_8));
+ }
+
+ private static Buffer gzip(String body) throws IOException {
+ Buffer buffer = new Buffer();
+ Okio.buffer(new GzipSink(buffer)).writeUtf8(body).close();
+ return buffer;
+ }
+
+ private static String gunzip(HttpEntity body) throws IOException {
+ InputStream in = new GZIPInputStream(body.getContent());
+ Buffer buffer = new Buffer();
+ byte[] temp = new byte[1024];
+ int read;
+ while ((read = in.read(temp)) != -1) {
+ buffer.write(temp, 0, read);
}
- String decodedContent = rspBodyBytes.toString("UTF-8");
- assertEquals(text, decodedContent);
+ return buffer.readUtf8();
}
}
diff --git a/okhttp-hpacktests/README.md b/okhttp-hpacktests/README.md
new file mode 100644
index 0000000..6b85c9a
--- /dev/null
+++ b/okhttp-hpacktests/README.md
@@ -0,0 +1,19 @@
+OkHttp HPACK tests
+==================
+
+These tests use the [hpack-test-case][1] project to validate OkHttp's HPACK
+implementation. The HPACK test cases are in a separate git submodule, so to
+initialize them, you must run:
+
+ git submodule init
+ git submodule update
+
+TODO
+----
+
+ * Add maven goal to avoid manual call to git submodule init.
+ * Make hpack-test-case update itself from git, and run new tests.
+ * Add maven goal to generate stories and a pull request to hpack-test-case
+ to have others validate our output.
+
+[1]: https://github.com/http2jp/hpack-test-case
diff --git a/okhttp-hpacktests/pom.xml b/okhttp-hpacktests/pom.xml
new file mode 100644
index 0000000..70a59f2
--- /dev/null
+++ b/okhttp-hpacktests/pom.xml
@@ -0,0 +1,56 @@
+<?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.2.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>okhttp-hpacktests</artifactId>
+ <name>OkHttp HPACK Tests</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.squareup.okio</groupId>
+ <artifactId>okio</artifactId>
+ </dependency>
+ <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>
+ <!-- Gson: Java to Json conversion -->
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ <scope>compile</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <!-- Do not deploy this as an artifact to Maven central. -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java
new file mode 100644
index 0000000..30e1a7b
--- /dev/null
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeInteropTest.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.spdy.hpackjson.Story;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static com.squareup.okhttp.internal.spdy.hpackjson.HpackJsonUtil.storiesForCurrentDraft;
+
+@RunWith(Parameterized.class)
+public class HpackDecodeInteropTest extends HpackDecodeTestBase {
+
+ public HpackDecodeInteropTest(Story story) {
+ super(story);
+ }
+
+ @Parameterized.Parameters(name="{0}")
+ public static Collection<Story[]> createStories() throws Exception {
+ return createStories(storiesForCurrentDraft());
+ }
+
+ @Test
+ public void testGoodDecoderInterop() throws Exception {
+ testDecoder();
+ }
+}
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java
new file mode 100644
index 0000000..1bd9b00
--- /dev/null
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDecodeTestBase.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.spdy.hpackjson.Case;
+import com.squareup.okhttp.internal.spdy.hpackjson.HpackJsonUtil;
+import com.squareup.okhttp.internal.spdy.hpackjson.Story;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import okio.Buffer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests Hpack implementation using https://github.com/http2jp/hpack-test-case/
+ */
+public class HpackDecodeTestBase {
+
+ /**
+ * Reads all stories in the folders provided, asserts if no story found.
+ */
+ protected static Collection<Story[]> createStories(String[] interopTests)
+ throws Exception {
+ List<Story[]> result = new ArrayList<>();
+ for (String interopTestName : interopTests) {
+ List<Story> stories = HpackJsonUtil.readStories(interopTestName);
+ if (stories.isEmpty()) {
+ fail("No stories for: " + interopTestName);
+ }
+ for (Story story : stories) {
+ result.add(new Story[] { story });
+ }
+ }
+ return result;
+ }
+
+ private final Buffer bytesIn = new Buffer();
+ private final HpackDraft10.Reader hpackReader = new HpackDraft10.Reader(4096, bytesIn);
+
+ private final Story story;
+
+ public HpackDecodeTestBase(Story story) {
+ this.story = story;
+ }
+
+ /**
+ * Expects wire to be set for all cases, and compares the decoder's output to
+ * expected headers.
+ */
+ protected void testDecoder() throws Exception {
+ testDecoder(story);
+ }
+
+ protected void testDecoder(Story story) throws Exception {
+ for (Case caze : story.getCases()) {
+ bytesIn.write(caze.getWire());
+ hpackReader.readHeaders();
+ assertSetEquals(String.format("seqno=%d", caze.getSeqno()), caze.getHeaders(),
+ hpackReader.getAndResetHeaderList());
+ }
+ }
+ /**
+ * Checks if {@code expected} and {@code observed} are equal when viewed as a
+ * set and headers are deduped.
+ *
+ * TODO: See if duped headers should be preserved on decode and verify.
+ */
+ private static void assertSetEquals(
+ String message, List<Header> expected, List<Header> observed) {
+ assertEquals(message, new LinkedHashSet<>(expected), new LinkedHashSet<>(observed));
+ }
+
+ protected Story getStory() {
+ return story;
+ }
+}
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java
new file mode 100644
index 0000000..a78dab5
--- /dev/null
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/HpackRoundTripTest.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.spdy.hpackjson.Case;
+import com.squareup.okhttp.internal.spdy.hpackjson.Story;
+import okio.Buffer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Collection;
+
+/**
+ * Tests for round-tripping headers through hpack..
+ */
+// TODO: update hpack-test-case with the output of our encoder.
+// This test will hide complementary bugs in the encoder and decoder,
+// We should test that the encoder is producing responses that are
+// d]
+@RunWith(Parameterized.class)
+public class HpackRoundTripTest extends HpackDecodeTestBase {
+
+ private static final String[] RAW_DATA = { "raw-data" };
+
+ @Parameterized.Parameters(name="{0}")
+ public static Collection<Story[]> getStories() throws Exception {
+ return createStories(RAW_DATA);
+ }
+
+ private Buffer bytesOut = new Buffer();
+ private HpackDraft10.Writer hpackWriter = new HpackDraft10.Writer(bytesOut);
+
+ public HpackRoundTripTest(Story story) {
+ super(story);
+ }
+
+ @Test
+ public void testRoundTrip() throws Exception {
+ Story story = getStory().clone();
+ // Mutate cases in base class.
+ for (Case caze : story.getCases()) {
+ hpackWriter.writeHeaders(caze.getHeaders());
+ caze.setWire(bytesOut.readByteString());
+ }
+
+ testDecoder(story);
+ }
+
+}
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java
new file mode 100644
index 0000000..d5d2728
--- /dev/null
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Case.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy.hpackjson;
+
+import com.squareup.okhttp.internal.spdy.Header;
+import okio.ByteString;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Representation of an individual case (set of headers and wire format).
+ * There are many cases for a single story. This class is used reflectively
+ * with Gson to parse stories.
+ */
+public class Case implements Cloneable {
+
+ private int seqno;
+ private String wire;
+ private List<Map<String, String>> headers;
+
+ public List<Header> getHeaders() {
+ List<Header> result = new ArrayList<>();
+ for (Map<String, String> inputHeader : headers) {
+ Map.Entry<String, String> entry = inputHeader.entrySet().iterator().next();
+ result.add(new Header(entry.getKey(), entry.getValue()));
+ }
+ return result;
+ }
+
+ public ByteString getWire() {
+ return ByteString.decodeHex(wire);
+ }
+
+ public int getSeqno() {
+ return seqno;
+ }
+
+ public void setWire(ByteString wire) {
+ this.wire = wire.hex();
+ }
+
+ @Override
+ protected Case clone() throws CloneNotSupportedException {
+ Case result = new Case();
+ result.seqno = seqno;
+ result.wire = wire;
+ result.headers = new ArrayList<>();
+ for (Map<String, String> header : headers) {
+ result.headers.add(new LinkedHashMap<String, String>(header));
+ }
+ return result;
+ }
+}
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java
new file mode 100644
index 0000000..9d721ab
--- /dev/null
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/HpackJsonUtil.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy.hpackjson;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Utilities for reading HPACK tests.
+ */
+public final class HpackJsonUtil {
+ private static final int CURRENT_DRAFT = 9;
+
+ private static final String STORY_RESOURCE_FORMAT = "/hpack-test-case/%s/story_%02d.json";
+
+ private static final Gson GSON = new GsonBuilder().create();
+
+ private static Story readStory(InputStream jsonResource) throws IOException {
+ return GSON.fromJson(new InputStreamReader(jsonResource, "UTF-8"), Story.class);
+ }
+
+ /** Iterate through the hpack-test-case resources, only picking stories for the current draft. */
+ public static String[] storiesForCurrentDraft() throws URISyntaxException {
+ File testCaseDirectory = new File(HpackJsonUtil.class.getResource("/hpack-test-case").toURI());
+ List<String> storyNames = new ArrayList<String>();
+ for (File path : testCaseDirectory.listFiles()) {
+ if (path.isDirectory() && Arrays.asList(path.list()).contains("story_00.json")) {
+ try {
+ Story firstStory = readStory(new FileInputStream(new File(path, "story_00.json")));
+ if (firstStory.getDraft() == CURRENT_DRAFT) {
+ storyNames.add(path.getName());
+ }
+ } catch (IOException ignored) {
+ // Skip this path.
+ }
+ }
+ }
+ return storyNames.toArray(new String[storyNames.size()]);
+ }
+
+ /**
+ * Reads stories named "story_xx.json" from the folder provided.
+ */
+ public static List<Story> readStories(String testFolderName) throws Exception {
+ List<Story> result = new ArrayList<>();
+ int i = 0;
+ while (true) { // break after last test.
+ String storyResourceName = String.format(STORY_RESOURCE_FORMAT, testFolderName, i);
+ InputStream storyInputStream = HpackJsonUtil.class.getResourceAsStream(storyResourceName);
+ if (storyInputStream == null) {
+ break;
+ }
+ try {
+ Story story = readStory(storyInputStream);
+ story.setFileName(storyResourceName);
+ result.add(story);
+ i++;
+ } finally {
+ storyInputStream.close();
+ }
+ }
+ return result;
+ }
+
+ private HpackJsonUtil() { } // Utilities only.
+}
\ No newline at end of file
diff --git a/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java
new file mode 100644
index 0000000..5ff2b07
--- /dev/null
+++ b/okhttp-hpacktests/src/test/java/com/squareup/okhttp/internal/spdy/hpackjson/Story.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy.hpackjson;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Representation of one story, a set of request headers to encode or decode.
+ * This class is used reflectively with Gson to parse stories from files.
+ */
+public class Story implements Cloneable {
+
+ private transient String fileName;
+ private List<Case> cases;
+ private int draft;
+ private String description;
+
+ /**
+ * The filename is only used in the toString representation.
+ */
+ void setFileName(String fileName) {
+ this.fileName = fileName;
+ }
+
+ public List<Case> getCases() {
+ return cases;
+ }
+
+ /** We only expect stories that match the draft we've implemented to pass. */
+ public int getDraft() {
+ return draft;
+ }
+
+ @Override
+ public Story clone() throws CloneNotSupportedException {
+ Story story = new Story();
+ story.fileName = this.fileName;
+ story.cases = new ArrayList<>();
+ for (Case caze : cases) {
+ story.cases.add(caze.clone());
+ }
+ story.draft = draft;
+ story.description = description;
+ return story;
+ }
+
+ @Override
+ public String toString() {
+ // Used as the test name.
+ return fileName;
+ }
+}
diff --git a/okhttp-tests/pom.xml b/okhttp-tests/pom.xml
index 7c1573d..face99d 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.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>okhttp-tests</artifactId>
@@ -16,7 +16,6 @@
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
- <version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
@@ -24,9 +23,9 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>org.mortbay.jetty.npn</groupId>
- <artifactId>npn-boot</artifactId>
- <scope>provided</scope>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-urlconnection</artifactId>
+ <version>${project.version}</version>
</dependency>
<dependency>
@@ -41,4 +40,17 @@
<scope>test</scope>
</dependency>
</dependencies>
+
+ <build>
+ <plugins>
+ <!-- Do not deploy this as an artifact to Maven central. -->
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-deploy-plugin</artifactId>
+ <configuration>
+ <skip>true</skip>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
new file mode 100644
index 0000000..44c39a8
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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.Util;
+import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
+import com.squareup.okhttp.internal.http.RecordingProxySelector;
+import java.util.List;
+import javax.net.SocketFactory;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public final class AddressTest {
+ private SocketFactory socketFactory = SocketFactory.getDefault();
+ private Authenticator authenticator = AuthenticatorAdapter.INSTANCE;
+ private List<Protocol> protocols = Util.immutableList(Protocol.HTTP_1_1);
+ private List<ConnectionSpec> connectionSpecs = Util.immutableList(ConnectionSpec.MODERN_TLS);
+ private RecordingProxySelector proxySelector = new RecordingProxySelector();
+
+ @Test public void equalsAndHashcode() throws Exception {
+ Address a = new Address("square.com", 80, socketFactory, null, null, null,
+ authenticator, null, protocols, connectionSpecs, proxySelector);
+ Address b = new Address("square.com", 80, 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,
+ authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
+ Address b = new Address("square.com", 80, 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/AsyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
deleted file mode 100644
index c0afe53..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
+++ /dev/null
@@ -1,401 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-import com.squareup.okhttp.internal.RecordingHostnameVerifier;
-import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.mockwebserver.Dispatcher;
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicReference;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-
-public final class AsyncApiTest {
- private MockWebServer server = new MockWebServer();
- private OkHttpClient client = new OkHttpClient();
- private RecordingReceiver receiver = new RecordingReceiver();
-
- private static final SSLContext sslContext = SslContextBuilder.localhost();
- private HttpResponseCache cache;
-
- @Before public void setUp() throws Exception {
- String tmp = System.getProperty("java.io.tmpdir");
- File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
- cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
- }
-
- @After public void tearDown() throws Exception {
- server.shutdown();
- cache.delete();
- }
-
- @Test public void get() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("abc")
- .addHeader("Content-Type: text/plain"));
- server.play();
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .header("User-Agent", "AsyncApiTest")
- .build();
- client.enqueue(request, receiver);
-
- receiver.await(request.url())
- .assertCode(200)
- .assertContainsHeaders("Content-Type: text/plain")
- .assertBody("abc");
-
- assertTrue(server.takeRequest().getHeaders().contains("User-Agent: AsyncApiTest"));
- }
-
- @Test public void connectionPooling() throws Exception {
- server.enqueue(new MockResponse().setBody("abc"));
- server.enqueue(new MockResponse().setBody("def"));
- server.enqueue(new MockResponse().setBody("ghi"));
- server.play();
-
- client.enqueue(new Request.Builder().url(server.getUrl("/a")).build(), receiver);
- receiver.await(server.getUrl("/a")).assertBody("abc");
-
- client.enqueue(new Request.Builder().url(server.getUrl("/b")).build(), receiver);
- receiver.await(server.getUrl("/b")).assertBody("def");
-
- client.enqueue(new Request.Builder().url(server.getUrl("/c")).build(), receiver);
- receiver.await(server.getUrl("/c")).assertBody("ghi");
-
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals(1, server.takeRequest().getSequenceNumber());
- assertEquals(2, server.takeRequest().getSequenceNumber());
- }
-
- @Test public void tls() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
- server.enqueue(new MockResponse()
- .setBody("abc")
- .addHeader("Content-Type: text/plain"));
- server.play();
-
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .build();
- client.enqueue(request, receiver);
-
- receiver.await(request.url()).assertHandshake();
- }
-
- @Test public void recoverFromTlsHandshakeFailure() throws Exception {
- SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
- server.useHttps(socketFactory, false);
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
- server.enqueue(new MockResponse().setBody("abc"));
- server.play();
-
- final boolean disableTlsFallbackScsv = true;
- SSLSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .build();
- client.enqueue(request, receiver);
-
- receiver.await(request.url()).assertBody("abc");
- }
-
- @Test public void post() throws Exception {
- server.enqueue(new MockResponse().setBody("abc"));
- server.play();
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
- .build();
- client.enqueue(request, receiver);
-
- receiver.await(request.url())
- .assertCode(200)
- .assertBody("abc");
-
- RecordedRequest recordedRequest = server.takeRequest();
- assertEquals("def", recordedRequest.getUtf8Body());
- assertEquals("3", recordedRequest.getHeader("Content-Length"));
- assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
- }
-
- @Test public void conditionalCacheHit() throws Exception {
- server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
- server.enqueue(new MockResponse()
- .clearHeaders()
- .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
-
- client.setOkResponseCache(cache);
-
- Request request1 = new Request.Builder()
- .url(server.getUrl("/"))
- .build();
- client.enqueue(request1, receiver);
- receiver.await(request1.url()).assertCode(200).assertBody("A");
- assertNull(server.takeRequest().getHeader("If-None-Match"));
-
- Request request2 = new Request.Builder()
- .url(server.getUrl("/"))
- .build();
- client.enqueue(request2, receiver);
- receiver.await(request2.url()).assertCode(200).assertBody("A");
- assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
- }
-
- @Test public void conditionalCacheMiss() throws Exception {
- server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
- server.enqueue(new MockResponse().setBody("B"));
- server.play();
-
- client.setOkResponseCache(cache);
-
- Request request1 = new Request.Builder()
- .url(server.getUrl("/"))
- .build();
- client.enqueue(request1, receiver);
- receiver.await(request1.url()).assertCode(200).assertBody("A");
- assertNull(server.takeRequest().getHeader("If-None-Match"));
-
- Request request2 = new Request.Builder()
- .url(server.getUrl("/"))
- .build();
- client.enqueue(request2, receiver);
- receiver.await(request2.url()).assertCode(200).assertBody("B");
- assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
- }
-
- @Test public void redirect() throws Exception {
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /b")
- .addHeader("Test", "Redirect from /a to /b")
- .setBody("/a has moved!"));
- server.enqueue(new MockResponse()
- .setResponseCode(302)
- .addHeader("Location: /c")
- .addHeader("Test", "Redirect from /b to /c")
- .setBody("/b has moved!"));
- server.enqueue(new MockResponse().setBody("C"));
- server.play();
-
- Request request = new Request.Builder().url(server.getUrl("/a")).build();
- client.enqueue(request, receiver);
-
- receiver.await(server.getUrl("/c"))
- .assertCode(200)
- .assertBody("C")
- .redirectedBy()
- .assertCode(302)
- .assertContainsHeaders("Test: Redirect from /b to /c")
- .redirectedBy()
- .assertCode(301)
- .assertContainsHeaders("Test: Redirect from /a to /b");
-
- assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
- assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
- assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again!
- }
-
- @Test public void redirectWithRedirectsDisabled() throws Exception {
- client.setFollowProtocolRedirects(false);
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /b")
- .addHeader("Test", "Redirect from /a to /b")
- .setBody("/a has moved!"));
- server.play();
-
- Request request = new Request.Builder().url(server.getUrl("/a")).build();
- client.enqueue(request, receiver);
-
- receiver.await(server.getUrl("/a"))
- .assertCode(301)
- .assertBody("/a has moved!")
- .assertContainsHeaders("Location: /b");
- }
-
- @Test public void follow20Redirects() throws Exception {
- for (int i = 0; i < 20; i++) {
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /" + (i + 1))
- .setBody("Redirecting to /" + (i + 1)));
- }
- server.enqueue(new MockResponse().setBody("Success!"));
- server.play();
-
- Request request = new Request.Builder().url(server.getUrl("/0")).build();
- client.enqueue(request, receiver);
- receiver.await(server.getUrl("/20"))
- .assertCode(200)
- .assertBody("Success!");
- }
-
- @Test public void doesNotFollow21Redirects() throws Exception {
- for (int i = 0; i < 21; i++) {
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /" + (i + 1))
- .setBody("Redirecting to /" + (i + 1)));
- }
- server.play();
-
- Request request = new Request.Builder().url(server.getUrl("/0")).build();
- client.enqueue(request, receiver);
- receiver.await(server.getUrl("/20")).assertFailure("Too many redirects: 21");
- }
-
- @Test public void canceledBeforeResponseReadIsNeverDelivered() throws Exception {
- client.getDispatcher().setMaxRequests(1); // Force requests to be executed serially.
- server.setDispatcher(new Dispatcher() {
- char nextResponse = 'A';
- @Override public MockResponse dispatch(RecordedRequest request) {
- client.cancel("request A");
- return new MockResponse().setBody(Character.toString(nextResponse++));
- }
- });
- server.play();
-
- // Canceling a request after the server has received a request but before
- // it has delivered the response. That request will never be received to the
- // client.
- Request requestA = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
- client.enqueue(requestA, receiver);
- assertEquals("/a", server.takeRequest().getPath());
-
- // We then make a second request (not canceled) to make sure the receiver
- // has nothing left to wait for.
- Request requestB = new Request.Builder().url(server.getUrl("/b")).tag("request B").build();
- client.enqueue(requestB, receiver);
- assertEquals("/b", server.takeRequest().getPath());
- receiver.await(requestB.url()).assertBody("B");
-
- // At this point we know the receiver is ready: if it hasn't received 'A'
- // yet it never will.
- receiver.assertNoResponse(requestA.url());
- }
-
- @Test public void canceledAfterResponseIsDeliveredDoesNothing() throws Exception {
- server.enqueue(new MockResponse().setBody("A"));
- server.play();
-
- final CountDownLatch latch = new CountDownLatch(1);
- final AtomicReference<String> bodyRef = new AtomicReference<String>();
-
- Request request = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
- client.enqueue(request, new Response.Receiver() {
- @Override public void onFailure(Failure failure) {
- throw new AssertionError();
- }
-
- @Override public boolean onResponse(Response response) throws IOException {
- client.cancel("request A");
- bodyRef.set(response.body().string());
- latch.countDown();
- return true;
- }
- });
-
- latch.await();
- assertEquals("A", bodyRef.get());
- }
-
- @Test public void connectionReuseWhenResponseBodyConsumed() throws Exception {
- server.enqueue(new MockResponse().setBody("abc"));
- server.enqueue(new MockResponse().setBody("def"));
- server.play();
-
- Request request = new Request.Builder().url(server.getUrl("/a")).build();
- client.enqueue(request, new Response.Receiver() {
- @Override public void onFailure(Failure failure) {
- throw new AssertionError();
- }
- @Override public boolean onResponse(Response response) throws IOException {
- InputStream bytes = response.body().byteStream();
- assertEquals('a', bytes.read());
- assertEquals('b', bytes.read());
- assertEquals('c', bytes.read());
-
- // This request will share a connection with 'A' cause it's all done.
- client.enqueue(new Request.Builder().url(server.getUrl("/b")).build(), receiver);
- return true;
- }
- });
-
- receiver.await(server.getUrl("/b")).assertCode(200).assertBody("def");
- assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
- assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reuse!
- }
-
- @Test public void postBodyRetransmittedOnRedirect() throws Exception {
- server.enqueue(new MockResponse()
- .setResponseCode(302)
- .addHeader("Location: /b")
- .setBody("Moved to /b !"));
- server.enqueue(new MockResponse()
- .setBody("This is b."));
- server.play();
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .post(Request.Body.create(MediaType.parse("text/plain"), "body!"))
- .build();
- client.enqueue(request, receiver);
-
- receiver.await(server.getUrl("/b"))
- .assertCode(200)
- .assertBody("This is b.");
-
- RecordedRequest request1 = server.takeRequest();
- assertEquals("body!", request1.getUtf8Body());
- assertEquals("5", request1.getHeader("Content-Length"));
- assertEquals("text/plain; charset=utf-8", request1.getHeader("Content-Type"));
- assertEquals(0, request1.getSequenceNumber());
-
- RecordedRequest request2 = server.takeRequest();
- assertEquals("body!", request2.getUtf8Body());
- assertEquals("5", request2.getHeader("Content-Length"));
- assertEquals("text/plain; charset=utf-8", request2.getHeader("Content-Type"));
- assertEquals(1, request2.getSequenceNumber());
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java
new file mode 100644
index 0000000..e08adf3
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheControlTest.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2014 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.util.concurrent.TimeUnit;
+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.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class CacheControlTest {
+ @Test public void emptyBuilderIsEmpty() throws Exception {
+ CacheControl cacheControl = new CacheControl.Builder().build();
+ assertEquals("", cacheControl.toString());
+ assertFalse(cacheControl.noCache());
+ assertFalse(cacheControl.noStore());
+ assertEquals(-1, cacheControl.maxAgeSeconds());
+ assertEquals(-1, cacheControl.sMaxAgeSeconds());
+ assertFalse(cacheControl.isPublic());
+ assertFalse(cacheControl.mustRevalidate());
+ assertEquals(-1, cacheControl.maxStaleSeconds());
+ assertEquals(-1, cacheControl.minFreshSeconds());
+ assertFalse(cacheControl.onlyIfCached());
+ assertFalse(cacheControl.mustRevalidate());
+ }
+
+ @Test public void completeBuilder() throws Exception {
+ CacheControl cacheControl = new CacheControl.Builder()
+ .noCache()
+ .noStore()
+ .maxAge(1, TimeUnit.SECONDS)
+ .maxStale(2, TimeUnit.SECONDS)
+ .minFresh(3, TimeUnit.SECONDS)
+ .onlyIfCached()
+ .noTransform()
+ .build();
+ assertEquals("no-cache, no-store, max-age=1, max-stale=2, min-fresh=3, only-if-cached, "
+ + "no-transform", cacheControl.toString());
+ assertTrue(cacheControl.noCache());
+ assertTrue(cacheControl.noStore());
+ assertEquals(1, cacheControl.maxAgeSeconds());
+ assertEquals(2, cacheControl.maxStaleSeconds());
+ assertEquals(3, cacheControl.minFreshSeconds());
+ assertTrue(cacheControl.onlyIfCached());
+
+ // These members are accessible to response headers only.
+ assertEquals(-1, cacheControl.sMaxAgeSeconds());
+ assertFalse(cacheControl.isPublic());
+ assertFalse(cacheControl.mustRevalidate());
+ }
+
+ @Test public void parseEmpty() throws Exception {
+ CacheControl cacheControl = CacheControl.parse(
+ new Headers.Builder().set("Cache-Control", "").build());
+ assertEquals("", cacheControl.toString());
+ assertFalse(cacheControl.noCache());
+ assertFalse(cacheControl.noStore());
+ assertEquals(-1, cacheControl.maxAgeSeconds());
+ assertEquals(-1, cacheControl.sMaxAgeSeconds());
+ assertFalse(cacheControl.isPublic());
+ assertFalse(cacheControl.mustRevalidate());
+ assertEquals(-1, cacheControl.maxStaleSeconds());
+ assertEquals(-1, cacheControl.minFreshSeconds());
+ assertFalse(cacheControl.onlyIfCached());
+ assertFalse(cacheControl.mustRevalidate());
+ }
+
+ @Test public void parse() throws Exception {
+ String header = "no-cache, no-store, max-age=1, s-maxage=2, public, must-revalidate, "
+ + "max-stale=3, min-fresh=4, only-if-cached, no-transform";
+ CacheControl cacheControl = CacheControl.parse(new Headers.Builder()
+ .set("Cache-Control", header)
+ .build());
+ assertTrue(cacheControl.noCache());
+ assertTrue(cacheControl.noStore());
+ assertEquals(1, cacheControl.maxAgeSeconds());
+ assertEquals(2, cacheControl.sMaxAgeSeconds());
+ assertTrue(cacheControl.isPublic());
+ assertTrue(cacheControl.mustRevalidate());
+ assertEquals(3, cacheControl.maxStaleSeconds());
+ assertEquals(4, cacheControl.minFreshSeconds());
+ assertTrue(cacheControl.onlyIfCached());
+ assertTrue(cacheControl.noTransform());
+ assertEquals(header, cacheControl.toString());
+ }
+
+ @Test public void parseCacheControlAndPragmaAreCombined() {
+ Headers headers =
+ Headers.of("Cache-Control", "max-age=12", "Pragma", "must-revalidate", "Pragma", "public");
+ CacheControl cacheControl = CacheControl.parse(headers);
+ assertEquals("max-age=12, public, must-revalidate", cacheControl.toString());
+ }
+
+ @SuppressWarnings("RedundantStringConstructorCall") // Testing instance equality.
+ @Test public void parseCacheControlHeaderValueIsRetained() {
+ String value = new String("max-age=12");
+ Headers headers = Headers.of("Cache-Control", value);
+ CacheControl cacheControl = CacheControl.parse(headers);
+ assertSame(value, cacheControl.toString());
+ }
+
+ @Test public void parseCacheControlHeaderValueInvalidatedByPragma() {
+ Headers headers = Headers.of("Cache-Control", "max-age=12", "Pragma", "must-revalidate");
+ CacheControl cacheControl = CacheControl.parse(headers);
+ assertNull(cacheControl.headerValue);
+ }
+
+ @Test public void parseCacheControlHeaderValueInvalidatedByTwoValues() {
+ Headers headers = Headers.of("Cache-Control", "max-age=12", "Cache-Control", "must-revalidate");
+ CacheControl cacheControl = CacheControl.parse(headers);
+ assertNull(cacheControl.headerValue);
+ }
+
+ @Test public void parsePragmaHeaderValueIsNotRetained() {
+ Headers headers = Headers.of("Pragma", "must-revalidate");
+ CacheControl cacheControl = CacheControl.parse(headers);
+ assertNull(cacheControl.headerValue);
+ }
+
+ @Test public void computedHeaderValueIsCached() {
+ CacheControl cacheControl = new CacheControl.Builder()
+ .maxAge(2, TimeUnit.DAYS)
+ .build();
+ assertNull(cacheControl.headerValue);
+ assertEquals("max-age=172800", cacheControl.toString());
+ assertEquals("max-age=172800", cacheControl.headerValue);
+ cacheControl.headerValue = "Hi";
+ assertEquals("Hi", cacheControl.toString());
+ }
+
+ @Test public void timeDurationTruncatedToMaxValue() throws Exception {
+ CacheControl cacheControl = new CacheControl.Builder()
+ .maxAge(365 * 100, TimeUnit.DAYS) // Longer than Integer.MAX_VALUE seconds.
+ .build();
+ assertEquals(Integer.MAX_VALUE, cacheControl.maxAgeSeconds());
+ }
+
+ @Test public void secondsMustBeNonNegative() throws Exception {
+ CacheControl.Builder builder = new CacheControl.Builder();
+ try {
+ builder.maxAge(-1, TimeUnit.SECONDS);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void timePrecisionIsTruncatedToSeconds() throws Exception {
+ CacheControl cacheControl = new CacheControl.Builder()
+ .maxAge(4999, TimeUnit.MILLISECONDS)
+ .build();
+ assertEquals(4, cacheControl.maxAgeSeconds());
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
new file mode 100644
index 0000000..d422143
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
@@ -0,0 +1,2263 @@
+/*
+ * 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;
+
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.GzipSink;
+import okio.Okio;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import java.io.File;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.ResponseCache;
+import java.net.URL;
+import java.security.Principal;
+import java.security.cert.Certificate;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.NoSuchElementException;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/** Test caching with {@link OkUrlFactory}. */
+public final class CacheTest {
+ private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
+ @Override public boolean verify(String s, SSLSession sslSession) {
+ return true;
+ }
+ };
+
+ private static final SSLContext sslContext = SslContextBuilder.localhost();
+
+ @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
+ @Rule public MockWebServerRule serverRule = new MockWebServerRule();
+ @Rule public MockWebServerRule server2Rule = new MockWebServerRule();
+
+ private final OkHttpClient client = new OkHttpClient();
+ private MockWebServer server;
+ private MockWebServer server2;
+ private Cache cache;
+ private final CookieManager cookieManager = new CookieManager();
+
+ @Before public void setUp() throws Exception {
+ server = serverRule.get();
+ server.setProtocolNegotiationEnabled(false);
+ server2 = server2Rule.get();
+ cache = new Cache(cacheRule.getRoot(), Integer.MAX_VALUE);
+ client.setCache(cache);
+ CookieHandler.setDefault(cookieManager);
+ }
+
+ @After public void tearDown() throws Exception {
+ ResponseCache.setDefault(null);
+ CookieHandler.setDefault(null);
+ }
+
+ /**
+ * Test that response caching is consistent with the RI and the spec.
+ * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
+ */
+ @Test public void responseCachingByResponseCode() throws Exception {
+ // Test each documented HTTP/1.1 code, plus the first unused value in each range.
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+
+ // We can't test 100 because it's not really a response.
+ // assertCached(false, 100);
+ assertCached(false, 101);
+ assertCached(false, 102);
+ assertCached(true, 200);
+ assertCached(false, 201);
+ assertCached(false, 202);
+ assertCached(true, 203);
+ assertCached(true, 204);
+ assertCached(false, 205);
+ assertCached(false, 206); //Electing to not cache partial responses
+ assertCached(false, 207);
+ assertCached(true, 300);
+ assertCached(true, 301);
+ assertCached(true, 302);
+ assertCached(false, 303);
+ assertCached(false, 304);
+ assertCached(false, 305);
+ assertCached(false, 306);
+ assertCached(true, 307);
+ assertCached(true, 308);
+ assertCached(false, 400);
+ assertCached(false, 401);
+ assertCached(false, 402);
+ assertCached(false, 403);
+ assertCached(true, 404);
+ assertCached(true, 405);
+ assertCached(false, 406);
+ assertCached(false, 408);
+ assertCached(false, 409);
+ // the HTTP spec permits caching 410s, but the RI doesn't.
+ assertCached(true, 410);
+ assertCached(false, 411);
+ assertCached(false, 412);
+ assertCached(false, 413);
+ assertCached(true, 414);
+ assertCached(false, 415);
+ assertCached(false, 416);
+ assertCached(false, 417);
+ assertCached(false, 418);
+
+ assertCached(false, 500);
+ assertCached(true, 501);
+ assertCached(false, 502);
+ assertCached(false, 503);
+ assertCached(false, 504);
+ assertCached(false, 505);
+ assertCached(false, 506);
+ }
+
+ private void assertCached(boolean shouldPut, int responseCode) throws Exception {
+ server = new MockWebServer();
+ MockResponse mockResponse = new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setResponseCode(responseCode)
+ .setBody("ABCDE")
+ .addHeader("WWW-Authenticate: challenge");
+ if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) {
+ mockResponse.addHeader("Proxy-Authenticate: Basic realm=\"protected area\"");
+ } else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
+ mockResponse.addHeader("WWW-Authenticate: Basic realm=\"protected area\"");
+ }
+ server.enqueue(mockResponse);
+ server.start();
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals(responseCode, response.code());
+
+ // Exhaust the content stream.
+ response.body().string();
+
+ Response cached = cache.get(request);
+ if (shouldPut) {
+ assertNotNull(Integer.toString(responseCode), cached);
+ cached.body().close();
+ } else {
+ assertNull(Integer.toString(responseCode), cached);
+ }
+ server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
+ }
+
+ @Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
+ testResponseCaching(TransferKind.FIXED_LENGTH);
+ }
+
+ @Test public void responseCachingAndInputStreamSkipWithChunkedEncoding() throws IOException {
+ testResponseCaching(TransferKind.CHUNKED);
+ }
+
+ @Test public void responseCachingAndInputStreamSkipWithNoLengthHeaders() throws IOException {
+ testResponseCaching(TransferKind.END_OF_STREAM);
+ }
+
+ /**
+ * Skipping bytes in the input stream caused ResponseCache corruption.
+ * http://code.google.com/p/android/issues/detail?id=8175
+ */
+ private void testResponseCaching(TransferKind transferKind) throws IOException {
+ MockResponse mockResponse = new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setStatus("HTTP/1.1 200 Fantastic");
+ transferKind.setBody(mockResponse, "I love puppies but hate spiders", 1);
+ server.enqueue(mockResponse);
+
+ // Make sure that calling skip() doesn't omit bytes from the cache.
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ Response response1 = client.newCall(request).execute();
+
+ BufferedSource in1 = response1.body().source();
+ assertEquals("I love ", in1.readUtf8("I love ".length()));
+ in1.skip("puppies but hate ".length());
+ assertEquals("spiders", in1.readUtf8("spiders".length()));
+ assertTrue(in1.exhausted());
+ in1.close();
+ assertEquals(1, cache.getWriteSuccessCount());
+ assertEquals(0, cache.getWriteAbortCount());
+
+ Response response2 = client.newCall(request).execute();
+ BufferedSource in2 = response2.body().source();
+ assertEquals("I love puppies but hate spiders",
+ in2.readUtf8("I love puppies but hate spiders".length()));
+ assertEquals(200, response2.code());
+ assertEquals("Fantastic", response2.message());
+
+ assertTrue(in2.exhausted());
+ in2.close();
+ assertEquals(1, cache.getWriteSuccessCount());
+ assertEquals(0, cache.getWriteAbortCount());
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getHitCount());
+ }
+
+ @Test public void secureResponseCaching() throws IOException {
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setBody("ABC"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ Response response1 = client.newCall(request).execute();
+ BufferedSource in = response1.body().source();
+ assertEquals("ABC", in.readUtf8());
+
+ // OpenJDK 6 fails on this line, complaining that the connection isn't open yet
+ String suite = response1.handshake().cipherSuite();
+ List<Certificate> localCerts = response1.handshake().localCertificates();
+ List<Certificate> serverCerts = response1.handshake().peerCertificates();
+ Principal peerPrincipal = response1.handshake().peerPrincipal();
+ Principal localPrincipal = response1.handshake().localPrincipal();
+
+ Response response2 = client.newCall(request).execute(); // Cached!
+ assertEquals("ABC", response2.body().source().readUtf8());
+
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(1, cache.getHitCount());
+
+ assertEquals(suite, response2.handshake().cipherSuite());
+ assertEquals(localCerts, response2.handshake().localCertificates());
+ assertEquals(serverCerts, response2.handshake().peerCertificates());
+ assertEquals(peerPrincipal, response2.handshake().peerPrincipal());
+ assertEquals(localPrincipal, response2.handshake().localPrincipal());
+ }
+
+ @Test public void responseCachingAndRedirects() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+ .addHeader("Location: /foo"));
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setBody("ABC"));
+ server.enqueue(new MockResponse()
+ .setBody("DEF"));
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ Response response1 = client.newCall(request).execute();
+ assertEquals("ABC", response1.body().string());
+
+ Response response2 = client.newCall(request).execute(); // Cached!
+ assertEquals("ABC", response2.body().string());
+
+ assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects
+ assertEquals(2, cache.getNetworkCount());
+ assertEquals(2, cache.getHitCount());
+ }
+
+ @Test public void redirectToCachedResult() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("ABC"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+ .addHeader("Location: /foo"));
+ server.enqueue(new MockResponse()
+ .setBody("DEF"));
+
+ Request request1 = new Request.Builder().url(server.getUrl("/foo")).build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("ABC", response1.body().string());
+ RecordedRequest recordedRequest1 = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", recordedRequest1.getRequestLine());
+ assertEquals(0, recordedRequest1.getSequenceNumber());
+
+ Request request2 = new Request.Builder().url(server.getUrl("/bar")).build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("ABC", response2.body().string());
+ RecordedRequest recordedRequest2 = server.takeRequest();
+ assertEquals("GET /bar HTTP/1.1", recordedRequest2.getRequestLine());
+ assertEquals(1, recordedRequest2.getSequenceNumber());
+
+ // an unrelated request should reuse the pooled connection
+ Request request3 = new Request.Builder().url(server.getUrl("/baz")).build();
+ Response response3 = client.newCall(request3).execute();
+ assertEquals("DEF", response3.body().string());
+ RecordedRequest recordedRequest3 = server.takeRequest();
+ assertEquals("GET /baz HTTP/1.1", recordedRequest3.getRequestLine());
+ assertEquals(2, recordedRequest3.getSequenceNumber());
+ }
+
+ @Test public void secureResponseCachingAndRedirects() throws IOException {
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+ .addHeader("Location: /foo"));
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setBody("ABC"));
+ server.enqueue(new MockResponse()
+ .setBody("DEF"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+
+ Response response1 = get(server.getUrl("/"));
+ assertEquals("ABC", response1.body().string());
+ assertNotNull(response1.handshake().cipherSuite());
+
+ // Cached!
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("ABC", response2.body().string());
+ assertNotNull(response2.handshake().cipherSuite());
+
+ assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
+ assertEquals(2, cache.getHitCount());
+ assertEquals(response1.handshake().cipherSuite(), response2.handshake().cipherSuite());
+ }
+
+ /**
+ * We've had bugs where caching and cross-protocol redirects yield class
+ * cast exceptions internal to the cache because we incorrectly assumed that
+ * HttpsURLConnection was always HTTPS and HttpURLConnection was always HTTP;
+ * in practice redirects mean that each can do either.
+ *
+ * https://github.com/square/okhttp/issues/214
+ */
+ @Test public void secureResponseCachingAndProtocolRedirects() throws IOException {
+ server2.useHttps(sslContext.getSocketFactory(), false);
+ server2.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setBody("ABC"));
+ server2.enqueue(new MockResponse()
+ .setBody("DEF"));
+
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
+ .addHeader("Location: " + server2.getUrl("/")));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+
+ Response response1 = get(server.getUrl("/"));
+ assertEquals("ABC", response1.body().string());
+
+ // Cached!
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("ABC", response2.body().string());
+
+ assertEquals(4, cache.getRequestCount()); // 2 direct + 2 redirect = 4
+ assertEquals(2, cache.getHitCount());
+ }
+
+ @Test public void foundCachedWithExpiresHeader() throws Exception {
+ temporaryRedirectCachedWithCachingHeader(302, "Expires", formatDate(1, TimeUnit.HOURS));
+ }
+
+ @Test public void foundCachedWithCacheControlHeader() throws Exception {
+ temporaryRedirectCachedWithCachingHeader(302, "Cache-Control", "max-age=60");
+ }
+
+ @Test public void temporaryRedirectCachedWithExpiresHeader() throws Exception {
+ temporaryRedirectCachedWithCachingHeader(307, "Expires", formatDate(1, TimeUnit.HOURS));
+ }
+
+ @Test public void temporaryRedirectCachedWithCacheControlHeader() throws Exception {
+ temporaryRedirectCachedWithCachingHeader(307, "Cache-Control", "max-age=60");
+ }
+
+ @Test public void foundNotCachedWithoutCacheHeader() throws Exception {
+ temporaryRedirectNotCachedWithoutCachingHeader(302);
+ }
+
+ @Test public void temporaryRedirectNotCachedWithoutCacheHeader() throws Exception {
+ temporaryRedirectNotCachedWithoutCachingHeader(307);
+ }
+
+ private void temporaryRedirectCachedWithCachingHeader(
+ int responseCode, String headerName, String headerValue) throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(responseCode)
+ .addHeader(headerName, headerValue)
+ .addHeader("Location", "/a"));
+ server.enqueue(new MockResponse()
+ .addHeader(headerName, headerValue)
+ .setBody("a"));
+ server.enqueue(new MockResponse()
+ .setBody("b"));
+ server.enqueue(new MockResponse()
+ .setBody("c"));
+
+ URL url = server.getUrl("/");
+ assertEquals("a", get(url).body().string());
+ assertEquals("a", get(url).body().string());
+ }
+
+ private void temporaryRedirectNotCachedWithoutCachingHeader(int responseCode) throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(responseCode)
+ .addHeader("Location", "/a"));
+ server.enqueue(new MockResponse()
+ .setBody("a"));
+ server.enqueue(new MockResponse()
+ .setBody("b"));
+
+ URL url = server.getUrl("/");
+ assertEquals("a", get(url).body().string());
+ assertEquals("b", get(url).body().string());
+ }
+
+ @Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
+ testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
+ }
+
+ @Test public void serverDisconnectsPrematurelyWithChunkedEncoding() throws IOException {
+ testServerPrematureDisconnect(TransferKind.CHUNKED);
+ }
+
+ @Test public void serverDisconnectsPrematurelyWithNoLengthHeaders() throws IOException {
+ // Intentionally empty. This case doesn't make sense because there's no
+ // such thing as a premature disconnect when the disconnect itself
+ // indicates the end of the data stream.
+ }
+
+ private void testServerPrematureDisconnect(TransferKind transferKind) throws IOException {
+ MockResponse mockResponse = new MockResponse();
+ transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
+ server.enqueue(truncateViolently(mockResponse, 16));
+ server.enqueue(new MockResponse()
+ .setBody("Request #2"));
+
+ BufferedSource bodySource = get(server.getUrl("/")).body().source();
+ assertEquals("ABCDE", bodySource.readUtf8Line());
+ try {
+ bodySource.readUtf8Line();
+ fail("This implementation silently ignored a truncated HTTP body.");
+ } catch (IOException expected) {
+ } finally {
+ bodySource.close();
+ }
+
+ assertEquals(1, cache.getWriteAbortCount());
+ assertEquals(0, cache.getWriteSuccessCount());
+ Response response = get(server.getUrl("/"));
+ assertEquals("Request #2", response.body().string());
+ assertEquals(1, cache.getWriteAbortCount());
+ assertEquals(1, cache.getWriteSuccessCount());
+ }
+
+ @Test public void clientPrematureDisconnectWithContentLengthHeader() throws IOException {
+ testClientPrematureDisconnect(TransferKind.FIXED_LENGTH);
+ }
+
+ @Test public void clientPrematureDisconnectWithChunkedEncoding() throws IOException {
+ testClientPrematureDisconnect(TransferKind.CHUNKED);
+ }
+
+ @Test public void clientPrematureDisconnectWithNoLengthHeaders() throws IOException {
+ testClientPrematureDisconnect(TransferKind.END_OF_STREAM);
+ }
+
+ private void testClientPrematureDisconnect(TransferKind transferKind) throws IOException {
+ // Setting a low transfer speed ensures that stream discarding will time out.
+ MockResponse mockResponse = new MockResponse()
+ .throttleBody(6, 1, TimeUnit.SECONDS);
+ transferKind.setBody(mockResponse, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
+ server.enqueue(mockResponse);
+ server.enqueue(new MockResponse()
+ .setBody("Request #2"));
+
+ Response response1 = get(server.getUrl("/"));
+ BufferedSource in = response1.body().source();
+ assertEquals("ABCDE", in.readUtf8(5));
+ in.close();
+ try {
+ in.readByte();
+ fail("Expected an IllegalStateException because the source is closed.");
+ } catch (IllegalStateException expected) {
+ }
+
+ assertEquals(1, cache.getWriteAbortCount());
+ assertEquals(0, cache.getWriteSuccessCount());
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("Request #2", response2.body().string());
+ assertEquals(1, cache.getWriteAbortCount());
+ assertEquals(1, cache.getWriteSuccessCount());
+ }
+
+ @Test public void defaultExpirationDateFullyCachedForLessThan24Hours() throws Exception {
+ // last modified: 105 seconds ago
+ // served: 5 seconds ago
+ // default lifetime: (105 - 5) / 10 = 10 seconds
+ // expires: 10 seconds from served date = 5 seconds from now
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
+ .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
+ .setBody("A"));
+
+ URL url = server.getUrl("/");
+ Response response1 = get(url);
+ assertEquals("A", response1.body().string());
+
+ Response response2 = get(url);
+ assertEquals("A", response2.body().string());
+ assertNull(response2.header("Warning"));
+ }
+
+ @Test public void defaultExpirationDateConditionallyCached() throws Exception {
+ // last modified: 115 seconds ago
+ // served: 15 seconds ago
+ // default lifetime: (115 - 15) / 10 = 10 seconds
+ // expires: 10 seconds from served date = 5 seconds ago
+ String lastModifiedDate = formatDate(-115, TimeUnit.SECONDS);
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
+ // last modified: 105 days ago
+ // served: 5 days ago
+ // default lifetime: (105 - 5) / 10 = 10 days
+ // expires: 10 days from served date = 5 days from now
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
+ .addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
+ .setBody("A"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Response response = get(server.getUrl("/"));
+ assertEquals("A", response.body().string());
+ assertEquals("113 HttpURLConnection \"Heuristic expiration\"", response.header("Warning"));
+ }
+
+ @Test public void noDefaultExpirationForUrlsWithQueryString() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
+ .addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/?foo=bar");
+ assertEquals("A", get(url).body().string());
+ assertEquals("B", get(url).body().string());
+ }
+
+ @Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception {
+ String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception {
+ assertNotCached(new MockResponse()
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+ }
+
+ @Test public void expirationDateInTheFuture() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ }
+
+ @Test public void maxAgePreferredWithMaxAgeAndExpires() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void maxAgeInThePastWithDateAndLastModifiedHeaders() throws Exception {
+ String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Cache-Control: max-age=60"));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
+ // Chrome interprets max-age relative to the local clock. Both our cache
+ // and Firefox both use the earlier of the local and server's clock.
+ assertNotCached(new MockResponse()
+ .addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void maxAgeInTheFutureWithDateHeader() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void maxAgeInTheFutureWithNoDateHeader() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void maxAgeWithLastModifiedButNoServedDate() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void maxAgeInTheFutureWithDateAndLastModifiedHeaders() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+ .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void maxAgePreferredOverLowerSharedMaxAge() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
+ .addHeader("Cache-Control: s-maxage=60")
+ .addHeader("Cache-Control: max-age=180"));
+ }
+
+ @Test public void maxAgePreferredOverHigherMaxAge() throws Exception {
+ assertNotCached(new MockResponse()
+ .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
+ .addHeader("Cache-Control: s-maxage=180")
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void requestMethodOptionsIsNotCached() throws Exception {
+ testRequestMethod("OPTIONS", false);
+ }
+
+ @Test public void requestMethodGetIsCached() throws Exception {
+ testRequestMethod("GET", true);
+ }
+
+ @Test public void requestMethodHeadIsNotCached() throws Exception {
+ // We could support this but choose not to for implementation simplicity
+ testRequestMethod("HEAD", false);
+ }
+
+ @Test public void requestMethodPostIsNotCached() throws Exception {
+ // We could support this but choose not to for implementation simplicity
+ testRequestMethod("POST", false);
+ }
+
+ @Test public void requestMethodPutIsNotCached() throws Exception {
+ testRequestMethod("PUT", false);
+ }
+
+ @Test public void requestMethodDeleteIsNotCached() throws Exception {
+ testRequestMethod("DELETE", false);
+ }
+
+ @Test public void requestMethodTraceIsNotCached() throws Exception {
+ testRequestMethod("TRACE", false);
+ }
+
+ private void testRequestMethod(String requestMethod, boolean expectCached) throws Exception {
+ // 1. seed the cache (potentially)
+ // 2. expect a cache hit or miss
+ server.enqueue(new MockResponse()
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .addHeader("X-Response-ID: 1"));
+ server.enqueue(new MockResponse()
+ .addHeader("X-Response-ID: 2"));
+
+ URL url = server.getUrl("/");
+
+ Request request = new Request.Builder()
+ .url(url)
+ .method(requestMethod, requestBodyOrNull(requestMethod))
+ .build();
+ Response response1 = client.newCall(request).execute();
+ response1.body().close();
+ assertEquals("1", response1.header("X-Response-ID"));
+
+ Response response2 = get(url);
+ response2.body().close();
+ if (expectCached) {
+ assertEquals("1", response2.header("X-Response-ID"));
+ } else {
+ assertEquals("2", response2.header("X-Response-ID"));
+ }
+ }
+
+ private RequestBody requestBodyOrNull(String requestMethod) {
+ return (requestMethod.equals("POST") || requestMethod.equals("PUT"))
+ ? RequestBody.create(MediaType.parse("text/plain"), "foo")
+ : null;
+ }
+
+ @Test public void postInvalidatesCache() throws Exception {
+ testMethodInvalidates("POST");
+ }
+
+ @Test public void putInvalidatesCache() throws Exception {
+ testMethodInvalidates("PUT");
+ }
+
+ @Test public void deleteMethodInvalidatesCache() throws Exception {
+ testMethodInvalidates("DELETE");
+ }
+
+ private void testMethodInvalidates(String requestMethod) throws Exception {
+ // 1. seed the cache
+ // 2. invalidate it
+ // 3. expect a cache miss
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+ server.enqueue(new MockResponse()
+ .setBody("C"));
+
+ URL url = server.getUrl("/");
+
+ assertEquals("A", get(url).body().string());
+
+ Request request = new Request.Builder()
+ .url(url)
+ .method(requestMethod, requestBodyOrNull(requestMethod))
+ .build();
+ Response invalidate = client.newCall(request).execute();
+ assertEquals("B", invalidate.body().string());
+
+ assertEquals("C", get(url).body().string());
+ }
+
+ @Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception {
+ // 1. seed the cache
+ // 2. invalidate it with uncacheable response
+ // 3. expect a cache miss
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ server.enqueue(new MockResponse()
+ .setBody("B")
+ .setResponseCode(500));
+ server.enqueue(new MockResponse()
+ .setBody("C"));
+
+ URL url = server.getUrl("/");
+
+ assertEquals("A", get(url).body().string());
+
+ Request request = new Request.Builder()
+ .url(url)
+ .method("POST", requestBodyOrNull("POST"))
+ .build();
+ Response invalidate = client.newCall(request).execute();
+ assertEquals("B", invalidate.body().string());
+
+ assertEquals("C", get(url).body().string());
+ }
+
+ @Test public void etag() throws Exception {
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("ETag: v1"));
+ assertEquals("v1", conditionalRequest.getHeader("If-None-Match"));
+ }
+
+ @Test public void etagAndExpirationDateInThePast() throws Exception {
+ String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("ETag: v1")
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+ assertEquals("v1", conditionalRequest.getHeader("If-None-Match"));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void etagAndExpirationDateInTheFuture() throws Exception {
+ assertFullyCached(new MockResponse()
+ .addHeader("ETag: v1")
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ }
+
+ @Test public void cacheControlNoCache() throws Exception {
+ assertNotCached(new MockResponse()
+ .addHeader("Cache-Control: no-cache"));
+ }
+
+ @Test public void cacheControlNoCacheAndExpirationDateInTheFuture() throws Exception {
+ String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: no-cache"));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void pragmaNoCache() throws Exception {
+ assertNotCached(new MockResponse()
+ .addHeader("Pragma: no-cache"));
+ }
+
+ @Test public void pragmaNoCacheAndExpirationDateInTheFuture() throws Exception {
+ String lastModifiedDate = formatDate(-2, TimeUnit.HOURS);
+ RecordedRequest conditionalRequest = assertConditionallyCached(new MockResponse()
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .addHeader("Pragma: no-cache"));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void cacheControlNoStore() throws Exception {
+ assertNotCached(new MockResponse()
+ .addHeader("Cache-Control: no-store"));
+ }
+
+ @Test public void cacheControlNoStoreAndExpirationDateInTheFuture() throws Exception {
+ assertNotCached(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: no-store"));
+ }
+
+ @Test public void partialRangeResponsesDoNotCorruptCache() throws Exception {
+ // 1. request a range
+ // 2. request a full document, expecting a cache miss
+ server.enqueue(new MockResponse()
+ .setBody("AA")
+ .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .addHeader("Content-Range: bytes 1000-1001/2000"));
+ server.enqueue(new MockResponse()
+ .setBody("BB"));
+
+ URL url = server.getUrl("/");
+
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Range", "bytes=1000-1001")
+ .build();
+ Response range = client.newCall(request).execute();
+ assertEquals("AA", range.body().string());
+
+ assertEquals("BB", get(url).body().string());
+ }
+
+ @Test public void serverReturnsDocumentOlderThanCache() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+ server.enqueue(new MockResponse()
+ .setBody("B")
+ .addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
+
+ URL url = server.getUrl("/");
+
+ assertEquals("A", get(url).body().string());
+ assertEquals("A", get(url).body().string());
+ }
+
+ @Test public void clientSideNoStore() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("B"));
+
+ Request request1 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .cacheControl(new CacheControl.Builder().noStore().build())
+ .build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("A", response1.body().string());
+
+ Request request2 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("B", response2.body().string());
+ }
+
+ @Test public void nonIdentityEncodingAndConditionalCache() throws Exception {
+ assertNonIdentityEncodingCached(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+ }
+
+ @Test public void nonIdentityEncodingAndFullCache() throws Exception {
+ assertNonIdentityEncodingCached(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ }
+
+ private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
+ server.enqueue(response
+ .setBody(gzip("ABCABCABC"))
+ .addHeader("Content-Encoding: gzip"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ // At least three request/response pairs are required because after the first request is cached
+ // a different execution path might be taken. Thus modifications to the cache applied during
+ // the second request might not be visible until another request is performed.
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ }
+
+ @Test public void notModifiedSpecifiesEncoding() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody(gzip("ABCABCABC"))
+ .addHeader("Content-Encoding: gzip")
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED)
+ .addHeader("Content-Encoding: gzip"));
+ server.enqueue(new MockResponse()
+ .setBody("DEFDEFDEF"));
+
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ assertEquals("DEFDEFDEF", get(server.getUrl("/")).body().string());
+ }
+
+ /** https://github.com/square/okhttp/issues/947 */
+ @Test public void gzipAndVaryOnAcceptEncoding() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody(gzip("ABCABCABC"))
+ .addHeader("Content-Encoding: gzip")
+ .addHeader("Vary: Accept-Encoding")
+ .addHeader("Cache-Control: max-age=60"));
+ server.enqueue(new MockResponse()
+ .setBody("FAIL"));
+
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ assertEquals("ABCABCABC", get(server.getUrl("/")).body().string());
+ }
+
+ @Test public void conditionalCacheHitIsNotDoublePooled() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("ETag: v1")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .clearHeaders()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ ConnectionPool pool = ConnectionPool.getDefault();
+ pool.evictAll();
+ client.setConnectionPool(pool);
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals(1, client.getConnectionPool().getConnectionCount());
+ }
+
+ @Test public void expiresDateBeforeModifiedDate() throws Exception {
+ assertConditionallyCached(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(-2, TimeUnit.HOURS)));
+ }
+
+ @Test public void requestMaxAge() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
+ .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "max-age=30")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void requestMinFresh() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "min-fresh=120")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void requestMaxStale() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=120")
+ .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "max-stale=180")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("A", response.body().string());
+ assertEquals("110 HttpURLConnection \"Response is stale\"", response.header("Warning"));
+ }
+
+ @Test public void requestMaxStaleDirectiveWithNoValue() throws IOException {
+ // Add a stale response to the cache.
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=120")
+ .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+
+ // With max-stale, we'll return that stale response.
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "max-stale")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("A", response.body().string());
+ assertEquals("110 HttpURLConnection \"Response is stale\"", response.header("Warning"));
+ }
+
+ @Test public void requestMaxStaleNotHonoredWithMustRevalidate() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=120, must-revalidate")
+ .addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "max-stale=180")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException {
+ // (no responses enqueued)
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "only-if-cached")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertTrue(response.body().source().exhausted());
+ assertEquals(504, response.code());
+ assertEquals(1, cache.getRequestCount());
+ assertEquals(0, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ }
+
+ @Test public void requestOnlyIfCachedWithFullResponseCached() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "only-if-cached")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("A", response.body().string());
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(1, cache.getHitCount());
+ }
+
+ @Test public void requestOnlyIfCachedWithConditionalResponseCached() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "only-if-cached")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertTrue(response.body().source().exhausted());
+ assertEquals(504, response.code());
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ }
+
+ @Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "only-if-cached")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertTrue(response.body().source().exhausted());
+ assertEquals(504, response.code());
+ assertEquals(2, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ }
+
+ @Test public void requestCacheControlNoCache() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+ .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Cache-Control", "no-cache")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void requestPragmaNoCache() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-120, TimeUnit.SECONDS))
+ .addHeader("Date: " + formatDate(0, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Pragma", "no-cache")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void clientSuppliedIfModifiedSinceWithCachedResult() throws Exception {
+ MockResponse response = new MockResponse()
+ .addHeader("ETag: v3")
+ .addHeader("Cache-Control: max-age=0");
+ String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
+ RecordedRequest request =
+ assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate);
+ assertEquals(ifModifiedSinceDate, request.getHeader("If-Modified-Since"));
+ assertNull(request.getHeader("If-None-Match"));
+ }
+
+ @Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
+ String lastModifiedDate = formatDate(-3, TimeUnit.MINUTES);
+ MockResponse response = new MockResponse()
+ .addHeader("Last-Modified: " + lastModifiedDate)
+ .addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
+ .addHeader("Cache-Control: max-age=0");
+ RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1");
+ assertEquals("v1", request.getHeader("If-None-Match"));
+ assertNull(request.getHeader("If-Modified-Since"));
+ }
+
+ private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
+ String conditionValue) throws Exception {
+ server.enqueue(seed.setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+
+ Request request = new Request.Builder()
+ .url(url)
+ .header(conditionName, conditionValue)
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, response.code());
+ assertEquals("", response.body().string());
+
+ server.takeRequest(); // seed
+ return server.takeRequest();
+ }
+
+ /**
+ * For Last-Modified and Date headers, we should echo the date back in the
+ * exact format we were served.
+ */
+ @Test public void retainServedDateFormat() throws Exception {
+ // Serve a response with a non-standard date format that OkHttp supports.
+ Date lastModifiedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-1));
+ Date servedDate = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(-2));
+ DateFormat dateFormat = new SimpleDateFormat("EEE dd-MMM-yyyy HH:mm:ss z", Locale.US);
+ dateFormat.setTimeZone(TimeZone.getTimeZone("EDT"));
+ String lastModifiedString = dateFormat.format(lastModifiedDate);
+ String servedString = dateFormat.format(servedDate);
+
+ // This response should be conditionally cached.
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + lastModifiedString)
+ .addHeader("Expires: " + servedString)
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+
+ // The first request has no conditions.
+ RecordedRequest request1 = server.takeRequest();
+ assertNull(request1.getHeader("If-Modified-Since"));
+
+ // The 2nd request uses the server's date format.
+ RecordedRequest request2 = server.takeRequest();
+ assertEquals(lastModifiedString, request2.getHeader("If-Modified-Since"));
+ }
+
+ @Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("If-Modified-Since", formatDate(-24, TimeUnit.HOURS))
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, response.code());
+ assertEquals("", response.body().string());
+ }
+
+ @Test public void authorizationRequestFullyCached() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Authorization", "password")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("A", response.body().string());
+ assertEquals("A", get(url).body().string());
+ }
+
+ @Test public void contentLocationDoesNotPopulateCache() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Content-Location: /bar")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/foo")).body().string());
+ assertEquals("B", get(server.getUrl("/bar")).body().string());
+ }
+
+ @Test public void connectionIsReturnedToPoolAfterConditionalSuccess() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/a")).body().string());
+ assertEquals("A", get(server.getUrl("/a")).body().string());
+ assertEquals("B", get(server.getUrl("/b")).body().string());
+
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ assertEquals(2, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void statisticsConditionalCacheMiss() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+ server.enqueue(new MockResponse()
+ .setBody("C"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals(1, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ assertEquals("B", get(server.getUrl("/")).body().string());
+ assertEquals("C", get(server.getUrl("/")).body().string());
+ assertEquals(3, cache.getRequestCount());
+ assertEquals(3, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ }
+
+ @Test public void statisticsConditionalCacheHit() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals(1, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals(3, cache.getRequestCount());
+ assertEquals(3, cache.getNetworkCount());
+ assertEquals(2, cache.getHitCount());
+ }
+
+ @Test public void statisticsFullCacheHit() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals(1, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(0, cache.getHitCount());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals(3, cache.getRequestCount());
+ assertEquals(1, cache.getNetworkCount());
+ assertEquals(2, cache.getHitCount());
+ }
+
+ @Test public void varyMatchesChangedRequestHeaderField() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request frRequest = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .build();
+ Response frResponse = client.newCall(frRequest).execute();
+ assertEquals("A", frResponse.body().string());
+
+ Request enRequest = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "en-US")
+ .build();
+ Response enResponse = client.newCall(enRequest).execute();
+ assertEquals("B", enResponse.body().string());
+ }
+
+ @Test public void varyMatchesUnchangedRequestHeaderField() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .build();
+ Response response1 = client.newCall(request).execute();
+ assertEquals("A", response1.body().string());
+ Request request1 = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .build();
+ Response response2 = client.newCall(request1).execute();
+ assertEquals("A", response2.body().string());
+ }
+
+ @Test public void varyMatchesAbsentRequestHeaderField() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Foo")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ }
+
+ @Test public void varyMatchesAddedRequestHeaderField() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Foo")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Foo", "bar")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void varyMatchesRemovedRequestHeaderField() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Foo")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Foo", "bar")
+ .build();
+ Response fooresponse = client.newCall(request).execute();
+ assertEquals("A", fooresponse.body().string());
+ assertEquals("B", get(server.getUrl("/")).body().string());
+ }
+
+ @Test public void varyFieldsAreCaseInsensitive() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: ACCEPT-LANGUAGE")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .build();
+ Response response1 = client.newCall(request).execute();
+ assertEquals("A", response1.body().string());
+ Request request1 = new Request.Builder()
+ .url(url)
+ .header("accept-language", "fr-CA")
+ .build();
+ Response response2 = client.newCall(request1).execute();
+ assertEquals("A", response2.body().string());
+ }
+
+ @Test public void varyMultipleFieldsWithMatch() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language, Accept-Charset")
+ .addHeader("Vary: Accept-Encoding")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request request = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .header("Accept-Charset", "UTF-8")
+ .header("Accept-Encoding", "identity")
+ .build();
+ Response response1 = client.newCall(request).execute();
+ assertEquals("A", response1.body().string());
+ Request request1 = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .header("Accept-Charset", "UTF-8")
+ .header("Accept-Encoding", "identity")
+ .build();
+ Response response2 = client.newCall(request1).execute();
+ assertEquals("A", response2.body().string());
+ }
+
+ @Test public void varyMultipleFieldsWithNoMatch() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language, Accept-Charset")
+ .addHeader("Vary: Accept-Encoding")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request frRequest = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "fr-CA")
+ .header("Accept-Charset", "UTF-8")
+ .header("Accept-Encoding", "identity")
+ .build();
+ Response frResponse = client.newCall(frRequest).execute();
+ assertEquals("A", frResponse.body().string());
+ Request enRequest = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "en-CA")
+ .header("Accept-Charset", "UTF-8")
+ .header("Accept-Encoding", "identity")
+ .build();
+ Response enResponse = client.newCall(enRequest).execute();
+ assertEquals("B", enResponse.body().string());
+ }
+
+ @Test public void varyMultipleFieldValuesWithMatch() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request request1 = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "fr-CA, fr-FR")
+ .addHeader("Accept-Language", "en-US")
+ .build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("A", response1.body().string());
+
+ Request request2 = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "fr-CA, fr-FR")
+ .addHeader("Accept-Language", "en-US")
+ .build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("A", response2.body().string());
+ }
+
+ @Test public void varyMultipleFieldValuesWithNoMatch() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ Request request1 = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "fr-CA, fr-FR")
+ .addHeader("Accept-Language", "en-US")
+ .build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("A", response1.body().string());
+
+ Request request2 = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "fr-CA")
+ .addHeader("Accept-Language", "en-US")
+ .build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("B", response2.body().string());
+ }
+
+ @Test public void varyAsterisk() throws Exception {
+ server.enqueue( new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: *")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ assertEquals("B", get(server.getUrl("/")).body().string());
+ }
+
+ @Test public void varyAndHttps() throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Language")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+
+ URL url = server.getUrl("/");
+ Request request1 = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "en-US")
+ .build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("A", response1.body().string());
+
+ Request request2 = new Request.Builder()
+ .url(url)
+ .header("Accept-Language", "en-US")
+ .build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("A", response2.body().string());
+ }
+
+ @Test public void cachePlusCookies() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Set-Cookie: a=FIRST; domain=" + server.getCookieDomain() + ";")
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .addHeader("Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";")
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+ assertCookies(url, "a=FIRST");
+ assertEquals("A", get(url).body().string());
+ assertCookies(url, "a=SECOND");
+ }
+
+ @Test public void getHeadersReturnsNetworkEndToEndHeaders() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Allow: GET, HEAD")
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .addHeader("Allow: GET, HEAD, PUT")
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ Response response1 = get(server.getUrl("/"));
+ assertEquals("A", response1.body().string());
+ assertEquals("GET, HEAD", response1.header("Allow"));
+
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("A", response2.body().string());
+ assertEquals("GET, HEAD, PUT", response2.header("Allow"));
+ }
+
+ @Test public void getHeadersReturnsCachedHopByHopHeaders() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Transfer-Encoding: identity")
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .addHeader("Transfer-Encoding: none")
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ Response response1 = get(server.getUrl("/"));
+ assertEquals("A", response1.body().string());
+ assertEquals("identity", response1.header("Transfer-Encoding"));
+
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("A", response2.body().string());
+ assertEquals("identity", response2.header("Transfer-Encoding"));
+ }
+
+ @Test public void getHeadersDeletesCached100LevelWarnings() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Warning: 199 test danger")
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ Response response1 = get(server.getUrl("/"));
+ assertEquals("A", response1.body().string());
+ assertEquals("199 test danger", response1.header("Warning"));
+
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("A", response2.body().string());
+ assertEquals(null, response2.header("Warning"));
+ }
+
+ @Test public void getHeadersRetainsCached200LevelWarnings() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Warning: 299 test danger")
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ Response response1 = get(server.getUrl("/"));
+ assertEquals("A", response1.body().string());
+ assertEquals("299 test danger", response1.header("Warning"));
+
+ Response response2 = get(server.getUrl("/"));
+ assertEquals("A", response2.body().string());
+ assertEquals("299 test danger", response2.header("Warning"));
+ }
+
+ public void assertCookies(URL url, String... expectedCookies) throws Exception {
+ List<String> actualCookies = new ArrayList<>();
+ for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
+ actualCookies.add(cookie.toString());
+ }
+ assertEquals(Arrays.asList(expectedCookies), actualCookies);
+ }
+
+ @Test public void doNotCachePartialResponse() throws Exception {
+ assertNotCached(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_PARTIAL)
+ .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+ .addHeader("Content-Range: bytes 100-100/200")
+ .addHeader("Cache-Control: max-age=60"));
+ }
+
+ @Test public void conditionalHitUpdatesCache() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Allow: GET, HEAD")
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ // cache miss; seed the cache
+ Response response1 = get(server.getUrl("/a"));
+ assertEquals("A", response1.body().string());
+ assertEquals(null, response1.header("Allow"));
+
+ // conditional cache hit; update the cache
+ Response response2 = get(server.getUrl("/a"));
+ assertEquals(HttpURLConnection.HTTP_OK, response2.code());
+ assertEquals("A", response2.body().string());
+ assertEquals("GET, HEAD", response2.header("Allow"));
+
+ // full cache hit
+ Response response3 = get(server.getUrl("/a"));
+ assertEquals("A", response3.body().string());
+ assertEquals("GET, HEAD", response3.header("Allow"));
+
+ assertEquals(2, server.getRequestCount());
+ }
+
+ @Test public void responseSourceHeaderCached() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "only-if-cached")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("A", response.body().string());
+ }
+
+ @Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(-31, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse()
+ .setBody("B")
+ .addHeader("Cache-Control: max-age=30")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Response response = get(server.getUrl("/"));
+ assertEquals("B", response.body().string());
+ }
+
+ @Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A")
+ .addHeader("Cache-Control: max-age=0")
+ .addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
+ server.enqueue(new MockResponse()
+ .setResponseCode(304));
+
+ assertEquals("A", get(server.getUrl("/")).body().string());
+ Response response = get(server.getUrl("/"));
+ assertEquals("A", response.body().string());
+ }
+
+ @Test public void responseSourceHeaderFetched() throws IOException {
+ server.enqueue(new MockResponse()
+ .setBody("A"));
+
+ Response response = get(server.getUrl("/"));
+ assertEquals("A", response.body().string());
+ }
+
+ @Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
+ Headers.Builder headers = new Headers.Builder()
+ .add("Cache-Control: max-age=120");
+ Internal.instance.addLenient(headers, ": A");
+ server.enqueue(new MockResponse()
+ .setHeaders(headers.build())
+ .setBody("body"));
+
+ Response response = get(server.getUrl("/"));
+ assertEquals("A", response.header(""));
+ }
+
+ /**
+ * Old implementations of OkHttp's response cache wrote header fields like
+ * ":status: 200 OK". This broke our cached response parser because it split
+ * on the first colon. This regression test exists to help us read these old
+ * bad cache entries.
+ *
+ * https://github.com/square/okhttp/issues/227
+ */
+ @Test public void testGoldenCacheResponse() throws Exception {
+ cache.close();
+ server.enqueue(new MockResponse()
+ .clearHeaders()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ URL url = server.getUrl("/");
+ String urlKey = Util.md5Hex(url.toString());
+ String entryMetadata = ""
+ + "" + url + "\n"
+ + "GET\n"
+ + "0\n"
+ + "HTTP/1.1 200 OK\n"
+ + "7\n"
+ + ":status: 200 OK\n"
+ + ":version: HTTP/1.1\n"
+ + "etag: foo\n"
+ + "content-length: 3\n"
+ + "OkHttp-Received-Millis: " + System.currentTimeMillis() + "\n"
+ + "X-Android-Response-Source: NETWORK 200\n"
+ + "OkHttp-Sent-Millis: " + System.currentTimeMillis() + "\n"
+ + "\n"
+ + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA\n"
+ + "1\n"
+ + "MIIBpDCCAQ2gAwIBAgIBATANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDEw1qd2lsc29uLmxvY2FsMB4XDTEzMDgy"
+ + "OTA1MDE1OVoXDTEzMDgzMDA1MDE1OVowGDEWMBQGA1UEAxMNandpbHNvbi5sb2NhbDCBnzANBgkqhkiG9w0BAQEF"
+ + "AAOBjQAwgYkCgYEAlFW+rGo/YikCcRghOyKkJanmVmJSce/p2/jH1QvNIFKizZdh8AKNwojt3ywRWaDULA/RlCUc"
+ + "ltF3HGNsCyjQI/+Lf40x7JpxXF8oim1E6EtDoYtGWAseelawus3IQ13nmo6nWzfyCA55KhAWf4VipelEy8DjcuFK"
+ + "v6L0xwXnI0ECAwEAATANBgkqhkiG9w0BAQsFAAOBgQAuluNyPo1HksU3+Mr/PyRQIQS4BI7pRXN8mcejXmqyscdP"
+ + "7S6J21FBFeRR8/XNjVOp4HT9uSc2hrRtTEHEZCmpyoxixbnM706ikTmC7SN/GgM+SmcoJ1ipJcNcl8N0X6zym4dm"
+ + "yFfXKHu2PkTo7QFdpOJFvP3lIigcSZXozfmEDg==\n"
+ + "-1\n";
+ String entryBody = "abc";
+ String journalBody = ""
+ + "libcore.io.DiskLruCache\n"
+ + "1\n"
+ + "201105\n"
+ + "2\n"
+ + "\n"
+ + "CLEAN " + urlKey + " " + entryMetadata.length() + " " + entryBody.length() + "\n";
+ writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata);
+ writeFile(cache.getDirectory(), urlKey + ".1", entryBody);
+ writeFile(cache.getDirectory(), "journal", journalBody);
+ cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE);
+ client.setCache(cache);
+
+ Response response = get(url);
+ assertEquals(entryBody, response.body().string());
+ assertEquals("3", response.header("Content-Length"));
+ assertEquals("foo", response.header("etag"));
+ }
+
+ @Test public void evictAll() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+ client.getCache().evictAll();
+ assertEquals(0, client.getCache().getSize());
+ assertEquals("B", get(url).body().string());
+ }
+
+ @Test public void networkInterceptorInvokedForConditionalGet() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("ETag: v1")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ // Seed the cache.
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+
+ final AtomicReference<String> ifNoneMatch = new AtomicReference<>();
+ client.networkInterceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ ifNoneMatch.compareAndSet(null, chain.request().header("If-None-Match"));
+ return chain.proceed(chain.request());
+ }
+ });
+
+ // Confirm the value is cached and intercepted.
+ assertEquals("A", get(url).body().string());
+ assertEquals("v1", ifNoneMatch.get());
+ }
+
+ @Test public void networkInterceptorNotInvokedForFullyCached() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("A"));
+
+ // Seed the cache.
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+
+ // Confirm the interceptor isn't exercised.
+ client.networkInterceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ throw new AssertionError();
+ }
+ });
+ assertEquals("A", get(url).body().string());
+ }
+
+ @Test public void iterateCache() throws Exception {
+ // Put some responses in the cache.
+ server.enqueue(new MockResponse()
+ .setBody("a"));
+ URL urlA = server.getUrl("/a");
+ assertEquals("a", get(urlA).body().string());
+
+ server.enqueue(new MockResponse()
+ .setBody("b"));
+ URL urlB = server.getUrl("/b");
+ assertEquals("b", get(urlB).body().string());
+
+ server.enqueue(new MockResponse()
+ .setBody("c"));
+ URL urlC = server.getUrl("/c");
+ assertEquals("c", get(urlC).body().string());
+
+ // Confirm the iterator returns those responses...
+ Iterator<String> i = cache.urls();
+ assertTrue(i.hasNext());
+ assertEquals(urlA.toString(), i.next());
+ assertTrue(i.hasNext());
+ assertEquals(urlB.toString(), i.next());
+ assertTrue(i.hasNext());
+ assertEquals(urlC.toString(), i.next());
+
+ // ... and nothing else.
+ assertFalse(i.hasNext());
+ try {
+ i.next();
+ fail();
+ } catch (NoSuchElementException expected) {
+ }
+ }
+
+ @Test public void iteratorRemoveFromCache() throws Exception {
+ // Put a response in the cache.
+ server.enqueue(new MockResponse()
+ .addHeader("Cache-Control: max-age=60")
+ .setBody("a"));
+ URL url = server.getUrl("/a");
+ assertEquals("a", get(url).body().string());
+
+ // Remove it with iteration.
+ Iterator<String> i = cache.urls();
+ assertEquals(url.toString(), i.next());
+ i.remove();
+
+ // Confirm that subsequent requests suffer a cache miss.
+ server.enqueue(new MockResponse()
+ .setBody("b"));
+ assertEquals("b", get(url).body().string());
+ }
+
+ @Test public void iteratorRemoveWithoutNextThrows() throws Exception {
+ // Put a response in the cache.
+ server.enqueue(new MockResponse()
+ .setBody("a"));
+ URL url = server.getUrl("/a");
+ assertEquals("a", get(url).body().string());
+
+ Iterator<String> i = cache.urls();
+ assertTrue(i.hasNext());
+ try {
+ i.remove();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void iteratorRemoveOncePerCallToNext() throws Exception {
+ // Put a response in the cache.
+ server.enqueue(new MockResponse()
+ .setBody("a"));
+ URL url = server.getUrl("/a");
+ assertEquals("a", get(url).body().string());
+
+ Iterator<String> i = cache.urls();
+ assertEquals(url.toString(), i.next());
+ i.remove();
+
+ // Too many calls to remove().
+ try {
+ i.remove();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void elementEvictedBetweenHasNextAndNext() throws Exception {
+ // Put a response in the cache.
+ server.enqueue(new MockResponse()
+ .setBody("a"));
+ URL url = server.getUrl("/a");
+ assertEquals("a", get(url).body().string());
+
+ // The URL will remain available if hasNext() returned true...
+ Iterator<String> i = cache.urls();
+ assertTrue(i.hasNext());
+
+ // ...so even when we evict the element, we still get something back.
+ cache.evictAll();
+ assertEquals(url.toString(), i.next());
+
+ // Remove does nothing. But most importantly, it doesn't throw!
+ i.remove();
+ }
+
+ @Test public void elementEvictedBeforeHasNextIsOmitted() throws Exception {
+ // Put a response in the cache.
+ server.enqueue(new MockResponse()
+ .setBody("a"));
+ URL url = server.getUrl("/a");
+ assertEquals("a", get(url).body().string());
+
+ Iterator<String> i = cache.urls();
+ cache.evictAll();
+
+ // The URL was evicted before hasNext() made any promises.
+ assertFalse(i.hasNext());
+ try {
+ i.next();
+ fail();
+ } catch (NoSuchElementException expected) {
+ }
+ }
+
+ private Response get(URL url) throws IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .build();
+ return client.newCall(request).execute();
+ }
+
+
+ private void writeFile(File directory, String file, String content) throws IOException {
+ BufferedSink sink = Okio.buffer(Okio.sink(new File(directory, file)));
+ sink.writeUtf8(content);
+ sink.close();
+ }
+
+ /**
+ * @param delta the offset from the current date to use. Negative
+ * values yield dates in the past; positive values yield dates in the
+ * future.
+ */
+ private String formatDate(long delta, TimeUnit timeUnit) {
+ return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta)));
+ }
+
+ private String formatDate(Date date) {
+ DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+ rfc1123.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return rfc1123.format(date);
+ }
+
+ private void assertNotCached(MockResponse response) throws Exception {
+ server.enqueue(response.setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+ assertEquals("B", get(url).body().string());
+ }
+
+ /** @return the request with the conditional get headers. */
+ private RecordedRequest assertConditionallyCached(MockResponse response) throws Exception {
+ // scenario 1: condition succeeds
+ server.enqueue(response.setBody("A").setStatus("HTTP/1.1 200 A-OK"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ // scenario 2: condition fails
+ server.enqueue(response.setBody("B")
+ .setStatus("HTTP/1.1 200 B-OK"));
+ server.enqueue(new MockResponse()
+ .setStatus("HTTP/1.1 200 C-OK")
+ .setBody("C"));
+
+ URL valid = server.getUrl("/valid");
+ Response response1 = get(valid);
+ assertEquals("A", response1.body().string());
+ assertEquals(HttpURLConnection.HTTP_OK, response1.code());
+ assertEquals("A-OK", response1.message());
+ Response response2 = get(valid);
+ assertEquals("A", response2.body().string());
+ assertEquals(HttpURLConnection.HTTP_OK, response2.code());
+ assertEquals("A-OK", response2.message());
+
+ URL invalid = server.getUrl("/invalid");
+ Response response3 = get(invalid);
+ assertEquals("B", response3.body().string());
+ assertEquals(HttpURLConnection.HTTP_OK, response3.code());
+ assertEquals("B-OK", response3.message());
+ Response response4 = get(invalid);
+ assertEquals("C", response4.body().string());
+ assertEquals(HttpURLConnection.HTTP_OK, response4.code());
+ assertEquals("C-OK", response4.message());
+
+ server.takeRequest(); // regular get
+ return server.takeRequest(); // conditional get
+ }
+
+ private void assertFullyCached(MockResponse response) throws Exception {
+ server.enqueue(response.setBody("A"));
+ server.enqueue(response.setBody("B"));
+
+ URL url = server.getUrl("/");
+ assertEquals("A", get(url).body().string());
+ assertEquals("A", get(url).body().string());
+ }
+
+ /**
+ * Shortens the body of {@code response} but not the corresponding headers.
+ * Only useful to test how clients respond to the premature conclusion of
+ * the HTTP body.
+ */
+ private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
+ response.setSocketPolicy(DISCONNECT_AT_END);
+ Headers headers = response.getHeaders();
+ Buffer truncatedBody = new Buffer();
+ truncatedBody.write(response.getBody(), numBytesToKeep);
+ response.setBody(truncatedBody);
+ response.setHeaders(headers);
+ return response;
+ }
+
+ enum TransferKind {
+ CHUNKED() {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize)
+ throws IOException {
+ response.setChunkedBody(content, chunkSize);
+ }
+ },
+ FIXED_LENGTH() {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
+ response.setBody(content);
+ }
+ },
+ END_OF_STREAM() {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
+ response.setBody(content);
+ response.setSocketPolicy(DISCONNECT_AT_END);
+ response.removeHeader("Content-Length");
+ }
+ };
+
+ abstract void setBody(MockResponse response, Buffer content, int chunkSize) throws IOException;
+
+ void setBody(MockResponse response, String content, int chunkSize) throws IOException {
+ setBody(response, new Buffer().writeUtf8(content), chunkSize);
+ }
+ }
+
+ /** Returns a gzipped copy of {@code bytes}. */
+ public Buffer gzip(String data) throws IOException {
+ Buffer result = new Buffer();
+ BufferedSink sink = Okio.buffer(new GzipSink(result));
+ sink.writeUtf8(data);
+ sink.close();
+ return result;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
new file mode 100644
index 0000000..868c33c
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -0,0 +1,1719 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.DoubleInetAddressNetwork;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RecordingHostnameVerifier;
+import com.squareup.okhttp.internal.RecordingOkAuthenticator;
+import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Version;
+import com.squareup.okhttp.mockwebserver.Dispatcher;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.SocketException;
+import java.net.URL;
+import java.security.cert.Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.SynchronousQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLProtocolException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.GzipSink;
+import okio.Okio;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import static com.squareup.okhttp.internal.Internal.logger;
+import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class CallTest {
+ private static final SSLContext sslContext = SslContextBuilder.localhost();
+
+ @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
+ @Rule public final TestRule timeout = new Timeout(30_000);
+
+ @Rule public final MockWebServerRule server = new MockWebServerRule();
+ @Rule public final MockWebServerRule server2 = new MockWebServerRule();
+ private OkHttpClient client = new OkHttpClient();
+ private RecordingCallback callback = new RecordingCallback();
+ private TestLogHandler logHandler = new TestLogHandler();
+ private Cache cache;
+
+ @Before public void setUp() throws Exception {
+ client = new OkHttpClient();
+ callback = new RecordingCallback();
+ logHandler = new TestLogHandler();
+
+ cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE);
+ logger.addHandler(logHandler);
+ }
+
+ @After public void tearDown() throws Exception {
+ cache.delete();
+ logger.removeHandler(logHandler);
+ }
+
+ @Test public void get() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc").addHeader("Content-Type: text/plain"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("User-Agent", "SyncApiTest")
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertSuccessful()
+ .assertHeader("Content-Type", "text/plain")
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("GET", recordedRequest.getMethod());
+ assertEquals("SyncApiTest", recordedRequest.getHeader("User-Agent"));
+ assertEquals(0, recordedRequest.getBody().size());
+ assertNull(recordedRequest.getHeader("Content-Length"));
+ }
+
+ @Test public void lazilyEvaluateRequestUrl() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request1 = new Request.Builder()
+ .url("foo://bar?baz")
+ .build();
+ Request request2 = request1.newBuilder()
+ .url(server.getUrl("/"))
+ .build();
+ executeSynchronously(request2)
+ .assertCode(200)
+ .assertSuccessful()
+ .assertBody("abc");
+ }
+
+ @Ignore // TODO(jwilson): fix.
+ @Test public void invalidScheme() throws Exception {
+ try {
+ Request request = new Request.Builder()
+ .url("ftp://hostname/path")
+ .build();
+ executeSynchronously(request);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void getReturns500() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(500));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(500)
+ .assertNotSuccessful();
+ }
+
+ @Test public void get_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ get();
+ }
+
+ @Test public void get_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ get();
+ }
+
+ @Test public void getWithRequestBody() throws Exception {
+ server.enqueue(new MockResponse());
+
+ try {
+ new Request.Builder().method("GET", RequestBody.create(MediaType.parse("text/plain"), "abc"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void head() throws Exception {
+ server.enqueue(new MockResponse().addHeader("Content-Type: text/plain"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .head()
+ .header("User-Agent", "SyncApiTest")
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertHeader("Content-Type", "text/plain");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("HEAD", recordedRequest.getMethod());
+ assertEquals("SyncApiTest", recordedRequest.getHeader("User-Agent"));
+ assertEquals(0, recordedRequest.getBody().size());
+ assertNull(recordedRequest.getHeader("Content-Length"));
+ }
+
+ @Test public void head_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ head();
+ }
+
+ @Test public void head_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ head();
+ }
+
+ @Test public void post() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .post(RequestBody.create(MediaType.parse("text/plain"), "def"))
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals("def", recordedRequest.getBody().readUtf8());
+ assertEquals("3", recordedRequest.getHeader("Content-Length"));
+ assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+ }
+
+ @Test public void post_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ post();
+ }
+
+ @Test public void post_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ post();
+ }
+
+ @Test public void postZeroLength() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .method("POST", null)
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals(0, recordedRequest.getBody().size());
+ assertEquals("0", recordedRequest.getHeader("Content-Length"));
+ assertEquals(null, recordedRequest.getHeader("Content-Type"));
+ }
+
+ @Test public void postZeroLength_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postZeroLength();
+ }
+
+ @Test public void postZerolength_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ postZeroLength();
+ }
+
+ @Test public void postBodyRetransmittedAfterAuthorizationFail() throws Exception {
+ postBodyRetransmittedAfterAuthorizationFail("abc");
+ }
+
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postBodyRetransmittedAfterAuthorizationFail("abc");
+ }
+
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ 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);
+ postBodyRetransmittedAfterAuthorizationFail("");
+ }
+
+ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ postBodyRetransmittedAfterAuthorizationFail("");
+ }
+
+ private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(401));
+ server.enqueue(new MockResponse());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .method("POST", RequestBody.create(null, body))
+ .build();
+
+ String credential = Credentials.basic("jesse", "secret");
+ client.setAuthenticator(new RecordingOkAuthenticator(credential));
+
+ Response response = client.newCall(request).execute();
+ assertEquals(200, response.code());
+
+ RecordedRequest recordedRequest1 = server.takeRequest();
+ assertEquals("POST", recordedRequest1.getMethod());
+ assertEquals(body, recordedRequest1.getBody().readUtf8());
+ assertNull(recordedRequest1.getHeader("Authorization"));
+
+ RecordedRequest recordedRequest2 = server.takeRequest();
+ assertEquals("POST", recordedRequest2.getMethod());
+ assertEquals(body, recordedRequest2.getBody().readUtf8());
+ assertEquals(credential, recordedRequest2.getHeader("Authorization"));
+ }
+
+ @Test public void attemptAuthorization20Times() throws Exception {
+ for (int i = 0; i < 20; i++) {
+ server.enqueue(new MockResponse().setResponseCode(401));
+ }
+ server.enqueue(new MockResponse().setBody("Success!"));
+
+ String credential = Credentials.basic("jesse", "secret");
+ client.setAuthenticator(new RecordingOkAuthenticator(credential));
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertBody("Success!");
+ }
+
+ @Test public void doesNotAttemptAuthorization21Times() throws Exception {
+ for (int i = 0; i < 21; i++) {
+ server.enqueue(new MockResponse().setResponseCode(401));
+ }
+
+ String credential = Credentials.basic("jesse", "secret");
+ client.setAuthenticator(new RecordingOkAuthenticator(credential));
+
+ try {
+ client.newCall(new Request.Builder().url(server.getUrl("/0")).build()).execute();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("Too many follow-up requests: 21", expected.getMessage());
+ }
+ }
+
+ @Test public void delete() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .delete()
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("DELETE", recordedRequest.getMethod());
+ assertEquals(0, recordedRequest.getBody().size());
+ assertEquals("0", recordedRequest.getHeader("Content-Length"));
+ assertEquals(null, recordedRequest.getHeader("Content-Type"));
+ }
+
+ @Test public void delete_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ delete();
+ }
+
+ @Test public void delete_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ delete();
+ }
+
+ @Test public void put() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .put(RequestBody.create(MediaType.parse("text/plain"), "def"))
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("PUT", recordedRequest.getMethod());
+ assertEquals("def", recordedRequest.getBody().readUtf8());
+ assertEquals("3", recordedRequest.getHeader("Content-Length"));
+ assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+ }
+
+ @Test public void put_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ put();
+ }
+
+ @Test public void put_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ put();
+ }
+
+ @Test public void patch() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .patch(RequestBody.create(MediaType.parse("text/plain"), "def"))
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("PATCH", recordedRequest.getMethod());
+ assertEquals("def", recordedRequest.getBody().readUtf8());
+ assertEquals("3", recordedRequest.getHeader("Content-Length"));
+ assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+ }
+
+ @Test public void patch_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ patch();
+ }
+
+ @Test public void patch_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ patch();
+ }
+
+ @Test public void unspecifiedRequestBodyContentTypeDoesNotGetDefault() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .method("POST", RequestBody.create(null, "abc"))
+ .build();
+
+ executeSynchronously(request).assertCode(200);
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals(null, recordedRequest.getHeader("Content-Type"));
+ assertEquals("3", recordedRequest.getHeader("Content-Length"));
+ assertEquals("abc", recordedRequest.getBody().readUtf8());
+ }
+
+ @Test public void illegalToExecuteTwice() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("User-Agent", "SyncApiTest")
+ .build();
+
+ Call call = client.newCall(request);
+ call.execute();
+
+ try {
+ call.execute();
+ fail();
+ } catch (IllegalStateException e){
+ assertEquals("Already Executed", e.getMessage());
+ }
+
+ try {
+ call.enqueue(callback);
+ fail();
+ } catch (IllegalStateException e){
+ assertEquals("Already Executed", e.getMessage());
+ }
+
+ assertEquals("SyncApiTest", server.takeRequest().getHeader("User-Agent"));
+ }
+
+ @Test public void illegalToExecuteTwice_Async() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("User-Agent", "SyncApiTest")
+ .build();
+
+ Call call = client.newCall(request);
+ call.enqueue(callback);
+
+ try {
+ call.execute();
+ fail();
+ } catch (IllegalStateException e){
+ assertEquals("Already Executed", e.getMessage());
+ }
+
+ try {
+ call.enqueue(callback);
+ fail();
+ } catch (IllegalStateException e){
+ assertEquals("Already Executed", e.getMessage());
+ }
+
+ assertEquals("SyncApiTest", server.takeRequest().getHeader("User-Agent"));
+ }
+
+ @Test public void get_Async() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("User-Agent", "AsyncApiTest")
+ .build();
+ client.newCall(request).enqueue(callback);
+
+ callback.await(request.url())
+ .assertCode(200)
+ .assertHeader("Content-Type", "text/plain")
+ .assertBody("abc");
+
+ assertEquals("AsyncApiTest", server.takeRequest().getHeader("User-Agent"));
+ }
+
+ @Test public void exceptionThrownByOnResponseIsRedactedAndLogged() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/secret"))
+ .build();
+
+ client.newCall(request).enqueue(new Callback() {
+ @Override public void onFailure(Request request, IOException e) {
+ fail();
+ }
+
+ @Override public void onResponse(Response response) throws IOException {
+ throw new IOException("a");
+ }
+ });
+
+ assertEquals("INFO: Callback failure for call to " + server.getUrl("/") + "...",
+ logHandler.take());
+ }
+
+ @Test public void connectionPooling() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setBody("def"));
+ server.enqueue(new MockResponse().setBody("ghi"));
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/a")).build())
+ .assertBody("abc");
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/b")).build())
+ .assertBody("def");
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/c")).build())
+ .assertBody("ghi");
+
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ assertEquals(2, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void connectionPooling_Async() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setBody("def"));
+ server.enqueue(new MockResponse().setBody("ghi"));
+
+ client.newCall(new Request.Builder().url(server.getUrl("/a")).build()).enqueue(callback);
+ callback.await(server.getUrl("/a")).assertBody("abc");
+
+ client.newCall(new Request.Builder().url(server.getUrl("/b")).build()).enqueue(callback);
+ callback.await(server.getUrl("/b")).assertBody("def");
+
+ client.newCall(new Request.Builder().url(server.getUrl("/c")).build()).enqueue(callback);
+ callback.await(server.getUrl("/c")).assertBody("ghi");
+
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ assertEquals(2, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void connectionReuseWhenResponseBodyConsumed_Async() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setBody("def"));
+
+ Request request = new Request.Builder().url(server.getUrl("/a")).build();
+ client.newCall(request).enqueue(new Callback() {
+ @Override public void onFailure(Request request, IOException e) {
+ throw new AssertionError();
+ }
+
+ @Override public void onResponse(Response response) throws IOException {
+ InputStream bytes = response.body().byteStream();
+ assertEquals('a', bytes.read());
+ assertEquals('b', bytes.read());
+ assertEquals('c', bytes.read());
+
+ // This request will share a connection with 'A' cause it's all done.
+ client.newCall(new Request.Builder().url(server.getUrl("/b")).build()).enqueue(callback);
+ }
+ });
+
+ callback.await(server.getUrl("/b")).assertCode(200).assertBody("def");
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+ assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reuse!
+ }
+
+ @Test public void timeoutsUpdatedOnReusedConnections() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setBody("def").throttleBody(1, 750, TimeUnit.MILLISECONDS));
+
+ // First request: time out after 1000ms.
+ client.setReadTimeout(1000, TimeUnit.MILLISECONDS);
+ executeSynchronously(new Request.Builder().url(server.getUrl("/a")).build()).assertBody("abc");
+
+ // Second request: time out after 250ms.
+ client.setReadTimeout(250, TimeUnit.MILLISECONDS);
+ Request request = new Request.Builder().url(server.getUrl("/b")).build();
+ Response response = client.newCall(request).execute();
+ BufferedSource bodySource = response.body().source();
+ assertEquals('d', bodySource.readByte());
+
+ // The second byte of this request will be delayed by 750ms so we should time out after 250ms.
+ long startNanos = System.nanoTime();
+ try {
+ bodySource.readByte();
+ fail();
+ } catch (IOException expected) {
+ // Timed out as expected.
+ long elapsedNanos = System.nanoTime() - startNanos;
+ long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
+ assertTrue(String.format("Timed out: %sms", elapsedMillis), elapsedMillis < 500);
+ }
+ }
+
+ @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.setReadTimeout(100, TimeUnit.MILLISECONDS);
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ try {
+ // If this succeeds, too many requests were made.
+ client.newCall(request).execute();
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void tls() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ 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.getUrl("/")).build())
+ .assertHandshake();
+ }
+
+ @Test public void tls_Async() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ 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.getUrl("/"))
+ .build();
+ client.newCall(request).enqueue(callback);
+
+ callback.await(request.url()).assertHandshake();
+ }
+
+ @Test public void recoverWhenRetryOnConnectionFailureIsTrue() throws Exception {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse().setBody("retry success"));
+
+ Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ assertTrue(client.getRetryOnConnectionFailure());
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ Response response = client.newCall(request).execute();
+ assertEquals("retry success", response.body().string());
+ }
+
+ @Test public void noRecoverWhenRetryOnConnectionFailureIsFalse() throws Exception {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse().setBody("unreachable!"));
+
+ Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setRetryOnConnectionFailure(false);
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ try {
+ // If this succeeds, too many requests were made.
+ client.newCall(request).execute();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ @Test public void recoverFromTlsHandshakeFailure() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ suppressTlsFallbackScsv(client);
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/")).build())
+ .assertBody("abc");
+ }
+
+ @Test public void recoverFromTlsHandshakeFailure_tlsFallbackScsvEnabled() throws Exception {
+ final String tlsFallbackScsv = "TLS_FALLBACK_SCSV";
+ List<String> supportedCiphers =
+ Arrays.asList(sslContext.getSocketFactory().getSupportedCipherSuites());
+ if (!supportedCiphers.contains(tlsFallbackScsv)) {
+ // This only works if the client socket supports TLS_FALLBACK_SCSV.
+ return;
+ }
+
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+
+ RecordingSSLSocketFactory clientSocketFactory =
+ new RecordingSSLSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(clientSocketFactory);
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (SSLHandshakeException expected) {
+ }
+
+ List<SSLSocket> clientSockets = clientSocketFactory.getSocketsCreated();
+ SSLSocket firstSocket = clientSockets.get(0);
+ assertFalse(Arrays.asList(firstSocket.getEnabledCipherSuites()).contains(tlsFallbackScsv));
+ SSLSocket secondSocket = clientSockets.get(1);
+ assertTrue(Arrays.asList(secondSocket.getEnabledCipherSuites()).contains(tlsFallbackScsv));
+ }
+
+ @Test public void recoverFromTlsHandshakeFailure_Async() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ suppressTlsFallbackScsv(client);
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request).enqueue(callback);
+
+ callback.await(request.url()).assertBody("abc");
+ }
+
+ @Test public void noRecoveryFromTlsHandshakeFailureWhenTlsFallbackIsDisabled() throws Exception {
+ client.setConnectionSpecs(Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
+
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+
+ suppressTlsFallbackScsv(client);
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (SSLProtocolException expected) {
+ // RI response to the FAIL_HANDSHAKE
+ } catch (SSLHandshakeException expected) {
+ // Android's response to the FAIL_HANDSHAKE
+ }
+ }
+
+ @Test public void cleartextCallsFailWhenCleartextIsDisabled() throws Exception {
+ // Configure the client with only TLS configurations. No cleartext!
+ client.setConnectionSpecs(
+ Arrays.asList(ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS));
+
+ server.enqueue(new MockResponse());
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (SocketException expected) {
+ assertTrue(expected.getMessage().contains("exhausted connection specs"));
+ }
+ }
+
+ @Test public void setFollowSslRedirectsFalse() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ 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.getUrl("/")).build();
+ Response response = client.newCall(request).execute();
+ assertEquals(301, response.code());
+ }
+
+ @Test public void matchingPinnedCertificate() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ 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.getUrl("/")).build();
+ Response response1 = client.newCall(request1).execute();
+ CertificatePinner.Builder certificatePinnerBuilder = new CertificatePinner.Builder();
+ for (Certificate certificate : response1.handshake().peerCertificates()) {
+ certificatePinnerBuilder.add(server.get().getHostName(), CertificatePinner.pin(certificate));
+ }
+
+ // Make another request with certificate pinning. It should complete normally.
+ client.setCertificatePinner(certificatePinnerBuilder.build());
+ Request request2 = new Request.Builder().url(server.getUrl("/")).build();
+ Response response2 = client.newCall(request2).execute();
+ assertNotSame(response2.handshake(), response1.handshake());
+ }
+
+ @Test public void unmatchingPinnedCertificate() throws Exception {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse());
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ // Pin publicobject.com's cert.
+ client.setCertificatePinner(new CertificatePinner.Builder()
+ .add(server.get().getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
+ .build());
+
+ // When we pin the wrong certificate, connectivity fails.
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ assertTrue(expected.getMessage().startsWith("Certificate pinning failure!"));
+ }
+ }
+
+ @Test public void post_Async() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .post(RequestBody.create(MediaType.parse("text/plain"), "def"))
+ .build();
+ client.newCall(request).enqueue(callback);
+
+ callback.await(request.url())
+ .assertCode(200)
+ .assertBody("abc");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("def", recordedRequest.getBody().readUtf8());
+ assertEquals("3", recordedRequest.getHeader("Content-Length"));
+ assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
+ }
+
+ @Test public void postBodyRetransmittedOnFailureRecovery() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
+ server.enqueue(new MockResponse().setBody("def"));
+
+ // Seed the connection pool so we have something that can fail.
+ Request request1 = new Request.Builder().url(server.getUrl("/")).build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("abc", response1.body().string());
+
+ Request request2 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .post(RequestBody.create(MediaType.parse("text/plain"), "body!"))
+ .build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("def", response2.body().string());
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals(0, get.getSequenceNumber());
+
+ RecordedRequest post1 = server.takeRequest();
+ assertEquals("body!", post1.getBody().readUtf8());
+ assertEquals(1, post1.getSequenceNumber());
+
+ RecordedRequest post2 = server.takeRequest();
+ assertEquals("body!", post2.getBody().readUtf8());
+ assertEquals(0, post2.getSequenceNumber());
+ }
+
+ @Test public void cacheHit() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("ETag: v1")
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Vary: Accept-Charset")
+ .setBody("A"));
+
+ client.setCache(cache);
+
+ // Store a response in the cache.
+ URL url = server.getUrl("/");
+ Request cacheStoreRequest = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "fr-CA")
+ .addHeader("Accept-Charset", "UTF-8")
+ .build();
+ executeSynchronously(cacheStoreRequest)
+ .assertCode(200)
+ .assertBody("A");
+ assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+ // Hit that stored response.
+ Request cacheHitRequest = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "en-US") // Different, but Vary says it doesn't matter.
+ .addHeader("Accept-Charset", "UTF-8")
+ .build();
+ RecordedResponse cacheHit = executeSynchronously(cacheHitRequest);
+
+ // Check the merged response. The request is the application's original request.
+ cacheHit.assertCode(200)
+ .assertBody("A")
+ .assertHeader("ETag", "v1")
+ .assertRequestUrl(cacheStoreRequest.url())
+ .assertRequestHeader("Accept-Language", "en-US")
+ .assertRequestHeader("Accept-Charset", "UTF-8");
+
+ // Check the cached response. Its request contains only the saved Vary headers.
+ cacheHit.cacheResponse()
+ .assertCode(200)
+ .assertHeader("ETag", "v1")
+ .assertRequestMethod("GET")
+ .assertRequestUrl(cacheStoreRequest.url())
+ .assertRequestHeader("Accept-Language")
+ .assertRequestHeader("Accept-Charset", "UTF-8");
+
+ cacheHit.assertNoNetworkResponse();
+ }
+
+ @Test public void conditionalCacheHit() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("ETag: v1")
+ .addHeader("Vary: Accept-Charset")
+ .addHeader("Donut: a")
+ .setBody("A"));
+ server.enqueue(new MockResponse().clearHeaders()
+ .addHeader("Donut: b")
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ client.setCache(cache);
+
+ // Store a response in the cache.
+ URL url = server.getUrl("/");
+ Request cacheStoreRequest = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "fr-CA")
+ .addHeader("Accept-Charset", "UTF-8")
+ .build();
+ executeSynchronously(cacheStoreRequest)
+ .assertCode(200)
+ .assertHeader("Donut", "a")
+ .assertBody("A");
+ assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+ // Hit that stored response.
+ Request cacheHitRequest = new Request.Builder()
+ .url(url)
+ .addHeader("Accept-Language", "en-US") // Different, but Vary says it doesn't matter.
+ .addHeader("Accept-Charset", "UTF-8")
+ .build();
+ RecordedResponse cacheHit = executeSynchronously(cacheHitRequest);
+ assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+
+ // Check the merged response. The request is the application's original request.
+ cacheHit.assertCode(200)
+ .assertBody("A")
+ .assertHeader("Donut", "b")
+ .assertRequestUrl(cacheStoreRequest.url())
+ .assertRequestHeader("Accept-Language", "en-US")
+ .assertRequestHeader("Accept-Charset", "UTF-8")
+ .assertRequestHeader("If-None-Match"); // No If-None-Match on the user's request.
+
+ // Check the cached response. Its request contains only the saved Vary headers.
+ cacheHit.cacheResponse()
+ .assertCode(200)
+ .assertHeader("Donut", "a")
+ .assertHeader("ETag", "v1")
+ .assertRequestUrl(cacheStoreRequest.url())
+ .assertRequestHeader("Accept-Language") // No Vary on Accept-Language.
+ .assertRequestHeader("Accept-Charset", "UTF-8") // Because of Vary on Accept-Charset.
+ .assertRequestHeader("If-None-Match"); // This wasn't present in the original request.
+
+ // Check the network response. It has the caller's request, plus some caching headers.
+ cacheHit.networkResponse()
+ .assertCode(304)
+ .assertHeader("Donut", "b")
+ .assertRequestHeader("Accept-Language", "en-US")
+ .assertRequestHeader("Accept-Charset", "UTF-8")
+ .assertRequestHeader("If-None-Match", "v1"); // If-None-Match in the validation request.
+ }
+
+ @Test public void conditionalCacheHit_Async() throws Exception {
+ server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+ server.enqueue(new MockResponse()
+ .clearHeaders()
+ .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
+
+ client.setCache(cache);
+
+ Request request1 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request1).enqueue(callback);
+ callback.await(request1.url()).assertCode(200).assertBody("A");
+ assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+ Request request2 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request2).enqueue(callback);
+ callback.await(request2.url()).assertCode(200).assertBody("A");
+ assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+ }
+
+ @Test public void conditionalCacheMiss() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("ETag: v1")
+ .addHeader("Vary: Accept-Charset")
+ .addHeader("Donut: a")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .addHeader("Donut: b")
+ .setBody("B"));
+
+ client.setCache(cache);
+
+ Request cacheStoreRequest = new Request.Builder()
+ .url(server.getUrl("/"))
+ .addHeader("Accept-Language", "fr-CA")
+ .addHeader("Accept-Charset", "UTF-8")
+ .build();
+ executeSynchronously(cacheStoreRequest)
+ .assertCode(200)
+ .assertBody("A");
+ assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+ Request cacheMissRequest = new Request.Builder()
+ .url(server.getUrl("/"))
+ .addHeader("Accept-Language", "en-US") // Different, but Vary says it doesn't matter.
+ .addHeader("Accept-Charset", "UTF-8")
+ .build();
+ RecordedResponse cacheHit = executeSynchronously(cacheMissRequest);
+ assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+
+ // Check the user response. It has the application's original request.
+ cacheHit.assertCode(200)
+ .assertBody("B")
+ .assertHeader("Donut", "b")
+ .assertRequestUrl(cacheStoreRequest.url());
+
+ // Check the cache response. Even though it's a miss, we used the cache.
+ cacheHit.cacheResponse()
+ .assertCode(200)
+ .assertHeader("Donut", "a")
+ .assertHeader("ETag", "v1")
+ .assertRequestUrl(cacheStoreRequest.url());
+
+ // Check the network response. It has the network request, plus caching headers.
+ cacheHit.networkResponse()
+ .assertCode(200)
+ .assertHeader("Donut", "b")
+ .assertRequestHeader("If-None-Match", "v1") // If-None-Match in the validation request.
+ .assertRequestUrl(cacheStoreRequest.url());
+ }
+
+ @Test public void conditionalCacheMiss_Async() throws Exception {
+ server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
+ server.enqueue(new MockResponse().setBody("B"));
+
+ client.setCache(cache);
+
+ Request request1 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request1).enqueue(callback);
+ callback.await(request1.url()).assertCode(200).assertBody("A");
+ assertNull(server.takeRequest().getHeader("If-None-Match"));
+
+ Request request2 = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request2).enqueue(callback);
+ callback.await(request2.url()).assertCode(200).assertBody("B");
+ assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
+ }
+
+ @Test public void onlyIfCachedReturns504WhenNotCached() throws Exception {
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Cache-Control", "only-if-cached")
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(504)
+ .assertBody("")
+ .assertNoNetworkResponse()
+ .assertNoCacheResponse();
+ }
+
+ @Test public void redirect() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /b")
+ .addHeader("Test", "Redirect from /a to /b")
+ .setBody("/a has moved!"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(302)
+ .addHeader("Location: /c")
+ .addHeader("Test", "Redirect from /b to /c")
+ .setBody("/b has moved!"));
+ server.enqueue(new MockResponse().setBody("C"));
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/a")).build())
+ .assertCode(200)
+ .assertBody("C")
+ .priorResponse()
+ .assertCode(302)
+ .assertHeader("Test", "Redirect from /b to /c")
+ .priorResponse()
+ .assertCode(301)
+ .assertHeader("Test", "Redirect from /a to /b");
+
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+ assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
+ assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again!
+ }
+
+ @Test public void postRedirectsToGet() 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.getUrl("/page1"))
+ .post(RequestBody.create(MediaType.parse("text/plain"), "Request Body"))
+ .build()).execute();
+ assertEquals("Page 2", response.body().string());
+
+ RecordedRequest page1 = server.takeRequest();
+ assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine());
+ assertEquals("Request Body", page1.getBody().readUtf8());
+
+ RecordedRequest page2 = server.takeRequest();
+ assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
+ }
+
+ @Test public void redirectsDoNotIncludeTooManyCookies() throws Exception {
+ server2.enqueue(new MockResponse().setBody("Page 2"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ .addHeader("Location: " + server2.getUrl("/")));
+
+ CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
+ HttpCookie cookie = new HttpCookie("c", "cookie");
+ cookie.setDomain(server.get().getCookieDomain());
+ cookie.setPath("/");
+ String portList = Integer.toString(server.getPort());
+ cookie.setPortlist(portList);
+ cookieManager.getCookieStore().add(server.getUrl("/").toURI(), cookie);
+ client.setCookieHandler(cookieManager);
+
+ Response response = client.newCall(new Request.Builder()
+ .url(server.getUrl("/page1"))
+ .build()).execute();
+ assertEquals("Page 2", response.body().string());
+
+ RecordedRequest request1 = server.takeRequest();
+ assertEquals("$Version=\"1\"; c=\"cookie\";$Path=\"/\";$Domain=\""
+ + server.get().getCookieDomain()
+ + "\";$Port=\""
+ + portList
+ + "\"", request1.getHeader("Cookie"));
+
+ RecordedRequest request2 = server2.takeRequest();
+ assertNull(request2.getHeader("Cookie"));
+ }
+
+ @Test public void redirectsDoNotIncludeTooManyAuthHeaders() throws Exception {
+ server2.enqueue(new MockResponse().setBody("Page 2"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(401));
+ server.enqueue(new MockResponse()
+ .setResponseCode(302)
+ .addHeader("Location: " + server2.getUrl("/b")));
+
+ client.setAuthenticator(new RecordingOkAuthenticator(Credentials.basic("jesse", "secret")));
+
+ Request request = new Request.Builder().url(server.getUrl("/a")).build();
+ Response response = client.newCall(request).execute();
+ assertEquals("Page 2", response.body().string());
+
+ RecordedRequest redirectRequest = server2.takeRequest();
+ assertNull(redirectRequest.getHeader("Authorization"));
+ assertEquals("/b", redirectRequest.getPath());
+ }
+
+ @Test public void redirect_Async() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /b")
+ .addHeader("Test", "Redirect from /a to /b")
+ .setBody("/a has moved!"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(302)
+ .addHeader("Location: /c")
+ .addHeader("Test", "Redirect from /b to /c")
+ .setBody("/b has moved!"));
+ server.enqueue(new MockResponse().setBody("C"));
+
+ Request request = new Request.Builder().url(server.getUrl("/a")).build();
+ client.newCall(request).enqueue(callback);
+
+ callback.await(server.getUrl("/c"))
+ .assertCode(200)
+ .assertBody("C")
+ .priorResponse()
+ .assertCode(302)
+ .assertHeader("Test", "Redirect from /b to /c")
+ .priorResponse()
+ .assertCode(301)
+ .assertHeader("Test", "Redirect from /a to /b");
+
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+ assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
+ assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again!
+ }
+
+ @Test public void follow20Redirects() throws Exception {
+ for (int i = 0; i < 20; i++) {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /" + (i + 1))
+ .setBody("Redirecting to /" + (i + 1)));
+ }
+ server.enqueue(new MockResponse().setBody("Success!"));
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/0")).build())
+ .assertCode(200)
+ .assertBody("Success!");
+ }
+
+ @Test public void follow20Redirects_Async() throws Exception {
+ for (int i = 0; i < 20; i++) {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /" + (i + 1))
+ .setBody("Redirecting to /" + (i + 1)));
+ }
+ server.enqueue(new MockResponse().setBody("Success!"));
+
+ Request request = new Request.Builder().url(server.getUrl("/0")).build();
+ client.newCall(request).enqueue(callback);
+ callback.await(server.getUrl("/20"))
+ .assertCode(200)
+ .assertBody("Success!");
+ }
+
+ @Test public void doesNotFollow21Redirects() throws Exception {
+ for (int i = 0; i < 21; i++) {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /" + (i + 1))
+ .setBody("Redirecting to /" + (i + 1)));
+ }
+
+ try {
+ client.newCall(new Request.Builder().url(server.getUrl("/0")).build()).execute();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("Too many follow-up requests: 21", expected.getMessage());
+ }
+ }
+
+ @Test public void doesNotFollow21Redirects_Async() throws Exception {
+ for (int i = 0; i < 21; i++) {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /" + (i + 1))
+ .setBody("Redirecting to /" + (i + 1)));
+ }
+
+ Request request = new Request.Builder().url(server.getUrl("/0")).build();
+ client.newCall(request).enqueue(callback);
+ callback.await(server.getUrl("/20")).assertFailure("Too many follow-up requests: 21");
+ }
+
+ @Test public void canceledBeforeExecute() throws Exception {
+ Call call = client.newCall(new Request.Builder().url(server.getUrl("/a")).build());
+ call.cancel();
+
+ try {
+ call.execute();
+ fail();
+ } catch (IOException expected) {
+ }
+ assertEquals(0, server.getRequestCount());
+ }
+
+ @Test public void cancelTagImmediatelyAfterEnqueue() throws Exception {
+ Call call = client.newCall(new Request.Builder()
+ .url(server.getUrl("/a"))
+ .tag("request")
+ .build());
+ call.enqueue(callback);
+ client.cancel("request");
+ assertEquals(0, server.getRequestCount());
+ callback.await(server.getUrl("/a")).assertFailure("Canceled");
+ }
+
+ @Test public void cancelBeforeBodyIsRead() throws Exception {
+ server.enqueue(new MockResponse().setBody("def").throttleBody(1, 750, TimeUnit.MILLISECONDS));
+
+ final Call call = client.newCall(new Request.Builder().url(server.getUrl("/a")).build());
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future<Response> result = executor.submit(new Callable<Response>() {
+ @Override public Response call() throws Exception {
+ return call.execute();
+ }
+ });
+
+ Thread.sleep(100); // wait for it to go in flight.
+
+ call.cancel();
+ try {
+ result.get().body().bytes();
+ fail();
+ } catch (IOException expected) {
+ }
+ assertEquals(1, server.getRequestCount());
+ }
+
+ @Test public void cancelInFlightBeforeResponseReadThrowsIOE() throws Exception {
+ server.get().setDispatcher(new Dispatcher() {
+ @Override public MockResponse dispatch(RecordedRequest request) {
+ client.cancel("request");
+ return new MockResponse().setBody("A");
+ }
+ });
+
+ Request request = new Request.Builder().url(server.getUrl("/a")).tag("request").build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
+ @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ cancelInFlightBeforeResponseReadThrowsIOE();
+ }
+
+ @Test public void cancelInFlightBeforeResponseReadThrowsIOE_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ cancelInFlightBeforeResponseReadThrowsIOE();
+ }
+
+ /**
+ * This test puts a request in front of one that is to be canceled, so that it is canceled before
+ * I/O takes place.
+ */
+ @Test public void canceledBeforeIOSignalsOnFailure() throws Exception {
+ client.getDispatcher().setMaxRequests(1); // Force requests to be executed serially.
+ server.get().setDispatcher(new Dispatcher() {
+ char nextResponse = 'A';
+
+ @Override public MockResponse dispatch(RecordedRequest request) {
+ client.cancel("request B");
+ return new MockResponse().setBody(Character.toString(nextResponse++));
+ }
+ });
+
+ Request requestA = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+ client.newCall(requestA).enqueue(callback);
+ assertEquals("/a", server.takeRequest().getPath());
+
+ Request requestB = new Request.Builder().url(server.getUrl("/b")).tag("request B").build();
+ client.newCall(requestB).enqueue(callback);
+
+ callback.await(requestA.url()).assertBody("A");
+ // At this point we know the callback is ready, and that it will receive a cancel failure.
+ callback.await(requestB.url()).assertFailure("Canceled");
+ }
+
+ @Test public void canceledBeforeIOSignalsOnFailure_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ canceledBeforeIOSignalsOnFailure();
+ }
+
+ @Test public void canceledBeforeIOSignalsOnFailure_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ canceledBeforeIOSignalsOnFailure();
+ }
+
+ @Test public void canceledBeforeResponseReadSignalsOnFailure() throws Exception {
+ Request requestA = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+ final Call call = client.newCall(requestA);
+ server.get().setDispatcher(new Dispatcher() {
+ @Override public MockResponse dispatch(RecordedRequest request) {
+ call.cancel();
+ return new MockResponse().setBody("A");
+ }
+ });
+
+ call.enqueue(callback);
+ assertEquals("/a", server.takeRequest().getPath());
+
+ callback.await(requestA.url()).assertFailure("Canceled", "stream was reset: CANCEL",
+ "Socket closed");
+ }
+
+ @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ canceledBeforeResponseReadSignalsOnFailure();
+ }
+
+ @Test public void canceledBeforeResponseReadSignalsOnFailure_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ canceledBeforeResponseReadSignalsOnFailure();
+ }
+
+ /**
+ * There's a race condition where the cancel may apply after the stream has already been
+ * processed.
+ */
+ @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce() throws Exception {
+ server.enqueue(new MockResponse().setBody("A"));
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final AtomicReference<String> bodyRef = new AtomicReference<>();
+ final AtomicBoolean failureRef = new AtomicBoolean();
+
+ Request request = new Request.Builder().url(server.getUrl("/a")).tag("request A").build();
+ final Call call = client.newCall(request);
+ call.enqueue(new Callback() {
+ @Override public void onFailure(Request request, IOException e) {
+ failureRef.set(true);
+ latch.countDown();
+ }
+
+ @Override public void onResponse(Response response) throws IOException {
+ call.cancel();
+ try {
+ bodyRef.set(response.body().string());
+ } catch (IOException e) { // It is ok if this broke the stream.
+ bodyRef.set("A");
+ throw e; // We expect to not loop into onFailure in this case.
+ } finally {
+ latch.countDown();
+ }
+ }
+ });
+
+ latch.await();
+ assertEquals("A", bodyRef.get());
+ assertFalse(failureRef.get());
+ }
+
+ @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTP_2()
+ throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce();
+ }
+
+ @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_SPDY_3()
+ throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce();
+ }
+
+ @Test public void gzip() throws Exception {
+ Buffer gzippedBody = gzip("abcabcabc");
+ String bodySize = Long.toString(gzippedBody.size());
+
+ server.enqueue(new MockResponse()
+ .setBody(gzippedBody)
+ .addHeader("Content-Encoding: gzip"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ // Confirm that the user request doesn't have Accept-Encoding, and the user
+ // response doesn't have a Content-Encoding or Content-Length.
+ RecordedResponse userResponse = executeSynchronously(request);
+ userResponse.assertCode(200)
+ .assertRequestHeader("Accept-Encoding")
+ .assertHeader("Content-Encoding")
+ .assertHeader("Content-Length")
+ .assertBody("abcabcabc");
+
+ // But the network request doesn't lie. OkHttp used gzip for this call.
+ userResponse.networkResponse()
+ .assertHeader("Content-Encoding", "gzip")
+ .assertHeader("Content-Length", bodySize)
+ .assertRequestHeader("Accept-Encoding", "gzip");
+ }
+
+ @Test public void asyncResponseCanBeConsumedLater() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setBody("def"));
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("User-Agent", "SyncApiTest")
+ .build();
+
+ final BlockingQueue<Response> responseRef = new SynchronousQueue<>();
+ client.newCall(request).enqueue(new Callback() {
+ @Override public void onFailure(Request request, IOException e) {
+ throw new AssertionError();
+ }
+
+ @Override public void onResponse(Response response) throws IOException {
+ try {
+ responseRef.put(response);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ });
+
+ Response response = responseRef.take();
+ assertEquals(200, response.code());
+ assertEquals("abc", response.body().string());
+
+ // Make another request just to confirm that that connection can be reused...
+ executeSynchronously(new Request.Builder().url(server.getUrl("/")).build()).assertBody("def");
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
+ assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
+
+ // ... even before we close the response body!
+ response.body().close();
+ }
+
+ @Test public void userAgentIsIncludedByDefault() throws Exception {
+ server.enqueue(new MockResponse());
+
+ executeSynchronously(new Request.Builder().url(server.getUrl("/")).build());
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertTrue(recordedRequest.getHeader("User-Agent")
+ .matches(Version.userAgent()));
+ }
+
+ @Test public void setFollowRedirectsFalse() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(302)
+ .addHeader("Location: /b")
+ .setBody("A"));
+ server.enqueue(new MockResponse().setBody("B"));
+
+ client.setFollowRedirects(false);
+ RecordedResponse recordedResponse = executeSynchronously(
+ new Request.Builder().url(server.getUrl("/a")).build());
+
+ recordedResponse
+ .assertBody("A")
+ .assertCode(302);
+ }
+
+ @Test public void expect100ContinueNonEmptyRequestBody() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Expect", "100-continue")
+ .post(RequestBody.create(MediaType.parse("text/plain"), "abc"))
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertSuccessful();
+
+ assertEquals("abc", server.takeRequest().getUtf8Body());
+ }
+
+ @Test public void expect100ContinueEmptyRequestBody() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .header("Expect", "100-continue")
+ .post(RequestBody.create(MediaType.parse("text/plain"), ""))
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertSuccessful();
+ }
+
+ private RecordedResponse executeSynchronously(Request request) throws IOException {
+ Response response = client.newCall(request).execute();
+ return new RecordedResponse(request, response, null, response.body().string(), null);
+ }
+
+ /**
+ * Tests that use this will fail unless boot classpath is set. Ex. {@code
+ * -Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317}
+ */
+ private void enableProtocol(Protocol protocol) {
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.get().setProtocols(client.getProtocols());
+ }
+
+ private Buffer gzip(String data) throws IOException {
+ Buffer result = new Buffer();
+ BufferedSink sink = Okio.buffer(new GzipSink(result));
+ sink.writeUtf8(data);
+ sink.close();
+ return result;
+ }
+
+ private void assertContains(Collection<String> collection, String element) {
+ for (String c : collection) {
+ if (c != null && c.equalsIgnoreCase(element)) return;
+ }
+ fail("No " + element + " in " + collection);
+ }
+
+ private void assertContainsNoneMatching(List<String> headers, String pattern) {
+ for (String header : headers) {
+ if (header.matches(pattern)) {
+ fail("Header " + header + " matches " + pattern);
+ }
+ }
+ }
+
+ private static class RecordingSSLSocketFactory extends DelegatingSSLSocketFactory {
+
+ private List<SSLSocket> socketsCreated = new ArrayList<SSLSocket>();
+
+ public RecordingSSLSocketFactory(SSLSocketFactory delegate) {
+ super(delegate);
+ }
+
+ @Override
+ protected void configureSocket(SSLSocket sslSocket) throws IOException {
+ socketsCreated.add(sslSocket);
+ }
+
+ public List<SSLSocket> getSocketsCreated() {
+ return socketsCreated;
+ }
+ }
+
+ /**
+ * Used during tests that involve TLS connection fallback attempts. OkHttp includes the
+ * TLS_FALLBACK_SCSV cipher on fallback connections. See
+ * {@link com.squareup.okhttp.FallbackTestClientSocketFactory} for details.
+ */
+ private static void suppressTlsFallbackScsv(OkHttpClient client) {
+ FallbackTestClientSocketFactory clientSocketFactory =
+ new FallbackTestClientSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(clientSocketFactory);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
new file mode 100644
index 0000000..c5cea28
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 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 java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class CertificatePinnerTest {
+ static SslContextBuilder sslContextBuilder;
+
+ static KeyPair keyPairA;
+ static X509Certificate keypairACertificate1;
+ static String keypairACertificate1Pin;
+
+ static KeyPair keyPairB;
+ static X509Certificate keypairBCertificate1;
+ static String keypairBCertificate1Pin;
+
+ static {
+ try {
+ sslContextBuilder = new SslContextBuilder("example.com");
+
+ keyPairA = sslContextBuilder.generateKeyPair();
+ keypairACertificate1 = sslContextBuilder.selfSignedCertificate(keyPairA, "1");
+ keypairACertificate1Pin = CertificatePinner.pin(keypairACertificate1);
+
+ keyPairB = sslContextBuilder.generateKeyPair();
+ keypairBCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairB, "1");
+ keypairBCertificate1Pin = CertificatePinner.pin(keypairBCertificate1);
+ } catch (GeneralSecurityException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Test public void malformedPin() throws Exception {
+ CertificatePinner.Builder builder = new CertificatePinner.Builder();
+ try {
+ builder.add("example.com", "md5/DmxUShsZuNiqPQsX2Oi9uv2sCnw=");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void malformedBase64() throws Exception {
+ CertificatePinner.Builder builder = new CertificatePinner.Builder();
+ try {
+ builder.add("example.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw*");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ /** 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);
+
+ X509Certificate keypairBCertificate2 = sslContextBuilder.selfSignedCertificate(keyPairB, "2");
+ String keypairBCertificate2Pin = CertificatePinner.pin(keypairBCertificate2);
+
+ assertTrue(keypairACertificate1Pin.equals(keypairACertificate2Pin));
+ assertTrue(keypairBCertificate1Pin.equals(keypairBCertificate2Pin));
+ assertFalse(keypairACertificate1Pin.equals(keypairBCertificate1Pin));
+ }
+
+ @Test public void successfulCheck() throws Exception {
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add("example.com", keypairACertificate1Pin)
+ .build();
+
+ certificatePinner.check("example.com", keypairACertificate1);
+ }
+
+ @Test public void successfulMatchAcceptsAnyMatchingCertificate() throws Exception {
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add("example.com", keypairBCertificate1Pin)
+ .build();
+
+ certificatePinner.check("example.com", keypairACertificate1, keypairBCertificate1);
+ }
+
+ @Test public void unsuccessfulCheck() throws Exception {
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add("example.com", keypairACertificate1Pin)
+ .build();
+
+ try {
+ certificatePinner.check("example.com", keypairBCertificate1);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ }
+ }
+
+ @Test public void multipleCertificatesForOneHostname() throws Exception {
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add("example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+ .build();
+
+ certificatePinner.check("example.com", keypairACertificate1);
+ certificatePinner.check("example.com", keypairBCertificate1);
+ }
+
+ @Test public void multipleHostnamesForOneCertificate() throws Exception {
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add("example.com", keypairACertificate1Pin)
+ .add("www.example.com", keypairACertificate1Pin)
+ .build();
+
+ certificatePinner.check("example.com", keypairACertificate1);
+ certificatePinner.check("www.example.com", keypairACertificate1);
+ }
+
+ @Test public void absentHostnameMatches() throws Exception {
+ CertificatePinner certificatePinner = new CertificatePinner.Builder().build();
+ certificatePinner.check("example.com", keypairACertificate1);
+ }
+}
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 77ea831..ebeb698 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -18,13 +18,16 @@
import com.squareup.okhttp.internal.RecordingHostnameVerifier;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.HttpAuthenticator;
+import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
+import com.squareup.okhttp.internal.http.RecordingProxySelector;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Executor;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import org.junit.After;
@@ -52,6 +55,7 @@
private InetSocketAddress httpSocketAddress;
private ConnectionPool pool;
+ private FakeExecutor cleanupExecutor;
private Connection httpA;
private Connection httpB;
private Connection httpC;
@@ -67,39 +71,50 @@
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.play();
+ List<ConnectionSpec> connectionSpecs = Util.immutableList(
+ ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
+
+ httpServer.start();
httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
- null, HttpAuthenticator.SYSTEM_DEFAULT, null, Protocol.SPDY3_AND_HTTP11);
+ null, null, AuthenticatorAdapter.INSTANCE, null,
+ Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), connectionSpecs, proxySelector);
httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
httpServer.getPort());
- spdyServer.play();
+ spdyServer.start();
spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
- sslContext.getSocketFactory(), new RecordingHostnameVerifier(),
- HttpAuthenticator.SYSTEM_DEFAULT, null,Protocol.SPDY3_AND_HTTP11);
+ sslContext.getSocketFactory(), new RecordingHostnameVerifier(), CertificatePinner.DEFAULT,
+ AuthenticatorAdapter.INSTANCE, null, Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1),
+ connectionSpecs, 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);
+ Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress,
+ ConnectionSpec.CLEARTEXT);
+ Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress,
+ ConnectionSpec.MODERN_TLS);
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, null);
+ httpA.connect(200, 200, 200, null);
httpB = new Connection(pool, httpRoute);
- httpB.connect(200, 200, null);
+ httpB.connect(200, 200, 200, null);
httpC = new Connection(pool, httpRoute);
- httpC.connect(200, 200, null);
+ httpC.connect(200, 200, 200, null);
httpD = new Connection(pool, httpRoute);
- httpD.connect(200, 200, null);
+ httpD.connect(200, 200, 200, null);
httpE = new Connection(pool, httpRoute);
- httpE.connect(200, 200, null);
+ httpE.connect(200, 200, 200, null);
spdyA = new Connection(pool, spdyRoute);
- spdyA.connect(20000, 20000, null);
+ spdyA.connect(20000, 20000, 2000, null);
owner = new Object();
httpA.setOwner(owner);
@@ -113,12 +128,12 @@
httpServer.shutdown();
spdyServer.shutdown();
- Util.closeQuietly(httpA);
- Util.closeQuietly(httpB);
- Util.closeQuietly(httpC);
- Util.closeQuietly(httpD);
- Util.closeQuietly(httpE);
- Util.closeQuietly(spdyA);
+ 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 {
@@ -131,8 +146,9 @@
Connection connection = pool.get(httpAddress);
assertNull(connection);
- connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress));
- connection.connect(200, 200, null);
+ connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress,
+ ConnectionSpec.CLEARTEXT));
+ connection.connect(200, 200, 200, null);
connection.setOwner(owner);
assertEquals(0, pool.getConnectionCount());
@@ -140,7 +156,7 @@
assertNull(connection.getOwner());
assertEquals(1, pool.getConnectionCount());
assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
+ assertEquals(0, pool.getMultiplexedConnectionCount());
Connection recycledConnection = pool.get(httpAddress);
assertNull(connection.getOwner());
@@ -151,10 +167,31 @@
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);
}
@@ -170,10 +207,18 @@
assertPooled(pool);
}
- @Test public void idleConnectionNotReturned() throws Exception {
+ @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);
}
@@ -182,21 +227,35 @@
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);
- pool.get(spdyAddress); // Force the cleanup callable to run.
+ assertPooled(pool, httpB, httpA);
+
+ // The connections must be cleaned up.
+ pool.performCleanup();
assertPooled(pool);
}
@Test public void nonAliveConnectionNotReturned() throws Exception {
pool.recycle(httpA);
- httpA.close();
+
+ // 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);
}
@@ -224,6 +283,10 @@
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 {
@@ -232,32 +295,43 @@
fail();
} catch (IllegalArgumentException expected) {
}
+ // 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);
}
@Test public void validateIdleSpdyConnectionTimeout() throws Exception {
pool.share(spdyA);
- Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
- assertNull(pool.get(httpAddress));
+ assertPooled(pool, spdyA); // Connection should be in the pool.
+
+ Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
+ pool.performCleanup();
assertPooled(pool, spdyA); // Connection should still be in the pool.
- Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
- assertNull(pool.get(httpAddress));
- assertPooled(pool);
+
+ Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
+ pool.performCleanup();
+ assertPooled(pool); // Connection should have been removed.
}
@Test public void validateIdleHttpConnectionTimeout() throws Exception {
pool.recycle(httpA);
- Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.7));
- assertNull(pool.get(spdyAddress));
+ assertPooled(pool, httpA); // Connection should be in the pool.
+ cleanupExecutor.assertExecutionScheduled(true);
+
+ Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
+ pool.performCleanup();
assertPooled(pool, httpA); // Connection should still be in the pool.
- Thread.sleep((int) (KEEP_ALIVE_DURATION_MS * 0.4));
- assertNull(pool.get(spdyAddress));
- assertPooled(pool);
+
+ 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 {
@@ -268,51 +342,62 @@
pool.recycle(httpA);
assertEquals(1, pool.getConnectionCount());
assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
+ 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());
- // http C should be added and http A should be removed.
- pool.recycle(httpC);
- Thread.sleep(50);
+ pool.performCleanup();
+
+ // http A should be removed by cleanup.
assertEquals(2, pool.getConnectionCount());
assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
+ assertEquals(0, pool.getMultiplexedConnectionCount());
- // spdy A should be added and http B should be removed.
+ // spdy A should be added
pool.share(spdyA);
- Thread.sleep(50);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(3, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
assertEquals(1, pool.getSpdyConnectionCount());
- // http C should be removed from the pool.
+ 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.getSpdyConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
- // spdy A will be returned and kept in the pool.
+ // 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.getSpdyConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
- // Nothing should change.
+ // http C should be added to the pool
pool.recycle(httpC);
- Thread.sleep(50);
assertEquals(2, pool.getConnectionCount());
assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
// An http connection should be removed from the pool.
recycledHttpConnection = pool.get(httpAddress);
@@ -320,102 +405,98 @@
assertTrue(recycledHttpConnection.isAlive());
assertEquals(1, pool.getConnectionCount());
assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
- // spdy A will be returned and kept in the pool. Pool shouldn't change.
+ // 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.getSpdyConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
// http D should be added to the pool.
pool.recycle(httpD);
- Thread.sleep(50);
assertEquals(2, pool.getConnectionCount());
assertEquals(1, pool.getHttpConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
+
+ // http E should be added to the pool.
+ pool.recycle(httpE);
+ assertEquals(3, pool.getConnectionCount());
+ assertEquals(2, pool.getHttpConnectionCount());
assertEquals(1, pool.getSpdyConnectionCount());
- // http E should be added to the pool. spdy A should be removed from the pool.
- pool.recycle(httpE);
- Thread.sleep(50);
+ pool.performCleanup();
+
+ // spdy A should be removed from the pool by cleanup.
assertEquals(2, pool.getConnectionCount());
assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
+ assertEquals(0, pool.getMultiplexedConnectionCount());
}
- @Test public void connectionCleanup() throws IOException, InterruptedException {
+ @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);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+
+ // Give the cleanup callable time to run and settle down.
+ Thread.sleep(100);
// Kill http A.
- Util.closeQuietly(httpA);
+ Util.closeQuietly(httpA.getSocket());
- // Force pool to run a clean up.
- assertNotNull(pool.get(spdyAddress));
- Thread.sleep(50);
+ 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.getSpdyConnectionCount());
+ assertEquals(1, pool.getMultiplexedConnectionCount());
- Thread.sleep(KEEP_ALIVE_DURATION_MS);
- // Force pool to run a clean up.
- assertNull(pool.get(httpAddress));
- assertNull(pool.get(spdyAddress));
+ // Now let enough time pass for the connections to expire.
+ Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
- Thread.sleep(50);
-
+ // All remaining connections should be removed.
+ pool.performCleanup();
assertEquals(0, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
}
- // Tests to demonstrate Android bug http://b/18369687 and the solution to it.
- @Test public void connectionCleanup_draining() throws IOException, InterruptedException {
- ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
+ @Test public void maxIdleConnectionsLimitEnforced() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
- // Add 3 connections to the pool.
+ // Hit the max idle connections limit of 2.
pool.recycle(httpA);
pool.recycle(httpB);
- pool.share(spdyA);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+ Thread.sleep(100); // Give the cleanup callable time to run.
+ assertPooled(pool, httpB, httpA);
- // With no method calls made to the pool it will not clean up any connections.
- Thread.sleep(KEEP_ALIVE_DURATION_MS * 5);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+ // Adding httpC bumps httpA.
+ pool.recycle(httpC);
+ Thread.sleep(100); // Give the cleanup callable time to run.
+ assertPooled(pool, httpC, httpB);
- // Change the pool into a mode that will clean up connections.
- pool.enterDrainMode();
+ // Adding httpD bumps httpB.
+ pool.recycle(httpD);
+ Thread.sleep(100); // Give the cleanup callable time to run.
+ assertPooled(pool, httpD, httpC);
- // Give the drain thread a chance to run.
- for (int i = 0; i < 5; i++) {
- Thread.sleep(KEEP_ALIVE_DURATION_MS);
- if (pool.isDrained()) {
- break;
- }
- }
-
- // All connections should have drained.
- assertEquals(0, pool.getConnectionCount());
+ // Adding httpE bumps httpC.
+ pool.recycle(httpE);
+ Thread.sleep(100); // Give the cleanup callable time to run.
+ assertPooled(pool, httpE, httpD);
}
@Test public void evictAllConnections() throws Exception {
resetWithPoolSize(10);
pool.recycle(httpA);
- Util.closeQuietly(httpA); // Include a closed connection in the pool.
+ Util.closeQuietly(httpA.getSocket()); // Include a closed connection in the pool.
pool.recycle(httpB);
pool.share(spdyA);
int connectionCount = pool.getConnectionCount();
@@ -445,7 +526,57 @@
}
}
+ @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());
}
+
+ /**
+ * An executor that does not actually execute anything by default. See
+ * {@link #fakeExecute()}.
+ */
+ private static class FakeExecutor implements Executor {
+
+ 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;
+ }
+
+ public void assertExecutionScheduled(boolean expected) {
+ assertEquals(expected, runnable != null);
+ }
+
+ /**
+ * Executes the runnable.
+ */
+ public void fakeExecute() {
+ Runnable toRun = this.runnable;
+ this.runnable = null;
+ toRun.run();
+ }
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
new file mode 100644
index 0000000..df1e58f
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
@@ -0,0 +1,172 @@
+/*
+ * 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.http.AuthenticatorAdapter;
+
+import org.junit.Test;
+
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+public final class ConnectionSpecTest {
+
+ private static final Proxy PROXY = Proxy.NO_PROXY;
+ private static final InetSocketAddress INET_SOCKET_ADDRESS =
+ InetSocketAddress.createUnresolved("host", 443);
+ private static final Address HTTPS_ADDRESS = new Address(
+ INET_SOCKET_ADDRESS.getHostString(), INET_SOCKET_ADDRESS.getPort(), null, null, null, null,
+ AuthenticatorAdapter.INSTANCE, PROXY, Arrays.asList(Protocol.HTTP_1_1),
+ Arrays.asList(ConnectionSpec.MODERN_TLS), ProxySelector.getDefault());
+
+ @Test
+ public void cleartextBuilder() throws Exception {
+ ConnectionSpec cleartextSpec = new ConnectionSpec.Builder(false).build();
+ assertFalse(cleartextSpec.isTls());
+ }
+
+ @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)
+ .supportsTlsExtensions(true)
+ .build();
+ assertEquals(Arrays.asList(CipherSuite.TLS_RSA_WITH_RC4_128_MD5), tlsSpec.cipherSuites());
+ assertEquals(Arrays.asList(TlsVersion.TLS_1_2), tlsSpec.tlsVersions());
+ assertTrue(tlsSpec.supportsTlsExtensions());
+ }
+
+ @Test
+ public void tlsBuilder_defaultCiphers() throws Exception {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .supportsTlsExtensions(true)
+ .build();
+ assertNull(tlsSpec.cipherSuites());
+ assertEquals(Arrays.asList(TlsVersion.TLS_1_2), tlsSpec.tlsVersions());
+ assertTrue(tlsSpec.supportsTlsExtensions());
+ }
+
+ @Test
+ public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .supportsTlsExtensions(false)
+ .build();
+
+ SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ socket.setEnabledCipherSuites(new String[] {
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ });
+ socket.setEnabledProtocols(new String[] {
+ TlsVersion.TLS_1_2.javaName,
+ TlsVersion.TLS_1_1.javaName,
+ });
+
+ Route route = new Route(HTTPS_ADDRESS, PROXY, INET_SOCKET_ADDRESS, tlsSpec,
+ false /* shouldSendTlsFallbackIndicator */);
+ tlsSpec.apply(socket, route);
+
+ assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+
+ Set<String> expectedCipherSet =
+ createSet(
+ 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 {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .supportsTlsExtensions(false)
+ .build();
+
+ SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ socket.setEnabledCipherSuites(new String[] {
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ });
+ socket.setEnabledProtocols(new String[] {
+ TlsVersion.TLS_1_2.javaName,
+ TlsVersion.TLS_1_1.javaName,
+ });
+
+ Route route = new Route(HTTPS_ADDRESS, PROXY, INET_SOCKET_ADDRESS, tlsSpec,
+ true /* shouldSendTlsFallbackIndicator */);
+ tlsSpec.apply(socket, route);
+
+ assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+
+ Set<String> expectedCipherSet =
+ createSet(
+ 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")) {
+ expectedCipherSet.add("TLS_FALLBACK_SCSV");
+ }
+ assertEquals(expectedCipherSet, expectedCipherSet);
+ }
+
+ @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)
+ .supportsTlsExtensions(false)
+ .build();
+
+ SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ socket.setEnabledCipherSuites(new String[] {
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ });
+ socket.setEnabledProtocols(new String[] {
+ TlsVersion.TLS_1_2.javaName,
+ TlsVersion.TLS_1_1.javaName,
+ });
+
+ Route route = new Route(HTTPS_ADDRESS, PROXY, INET_SOCKET_ADDRESS, tlsSpec,
+ true /* shouldSendTlsFallbackIndicator */);
+ tlsSpec.apply(socket, route);
+
+ assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+
+ Set<String> expectedCipherSet = createSet(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);
+ }
+
+ private static Set<String> createSet(String... values) {
+ return new LinkedHashSet<String>(Arrays.asList(values));
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocket.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocket.java
index e98ff16..e13a50f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocket.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocket.java
@@ -28,7 +28,7 @@
import javax.net.ssl.SSLSocket;
/**
- * An {@link javax.net.ssl.SSLSocket} that delegates all {@link javax.net.ssl.SSLSocket} calls.
+ * An {@link javax.net.ssl.SSLSocket} that delegates all calls.
*/
public abstract class DelegatingSSLSocket extends SSLSocket {
protected final SSLSocket delegate;
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 2c41612..a14db22 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2014 Square Inc.
+ * Copyright (C) 2014 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
+ * 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,
@@ -19,13 +19,15 @@
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
+import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
/**
- * An {@link javax.net.ssl.SSLSocketFactory} that delegates all method calls.
+ * A {@link SSLSocketFactory} that delegates calls. Sockets can be configured after
+ * creation by overriding {@link #configureSocket(javax.net.ssl.SSLSocket)}.
*/
-public abstract class DelegatingSSLSocketFactory extends SSLSocketFactory {
+public class DelegatingSSLSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory delegate;
@@ -34,6 +36,43 @@
}
@Override
+ public SSLSocket createSocket() throws IOException {
+ SSLSocket sslSocket = (SSLSocket) delegate.createSocket();
+ configureSocket(sslSocket);
+ return sslSocket;
+ }
+
+ @Override
+ public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
+ configureSocket(sslSocket);
+ return sslSocket;
+ }
+
+ @Override
+ public SSLSocket createSocket(String host, int port, InetAddress localAddress, int localPort)
+ throws IOException, UnknownHostException {
+ SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
+ configureSocket(sslSocket);
+ return sslSocket;
+ }
+
+ @Override
+ public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+ SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
+ configureSocket(sslSocket);
+ return sslSocket;
+ }
+
+ @Override
+ public SSLSocket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort)
+ throws IOException {
+ SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
+ configureSocket(sslSocket);
+ return sslSocket;
+ }
+
+ @Override
public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}
@@ -44,35 +83,14 @@
}
@Override
- public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
+ public SSLSocket createSocket(Socket socket, String host, int port, boolean autoClose)
throws IOException {
- return (SSLSocket) delegate.createSocket(s, host, port, autoClose);
+ SSLSocket sslSocket = (SSLSocket) delegate.createSocket(socket, host, port, autoClose);
+ configureSocket(sslSocket);
+ return sslSocket;
}
- @Override
- public SSLSocket createSocket() throws IOException {
- return (SSLSocket) delegate.createSocket();
- }
-
- @Override
- public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
- return (SSLSocket) delegate.createSocket(host, port);
- }
-
- @Override
- public SSLSocket createSocket(String host, int port, InetAddress localHost,
- int localPort) throws IOException, UnknownHostException {
- return (SSLSocket) delegate.createSocket(host, port, localHost, localPort);
- }
-
- @Override
- public SSLSocket createSocket(InetAddress host, int port) throws IOException {
- return (SSLSocket) delegate.createSocket(host, port);
- }
-
- @Override
- public SSLSocket createSocket(InetAddress address, int port,
- InetAddress localAddress, int localPort) throws IOException {
- return (SSLSocket) delegate.createSocket(address, port, localAddress, localPort);
+ protected void configureSocket(SSLSocket sslSocket) throws IOException {
+ // No-op by default.
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java
new file mode 100644
index 0000000..ef24aaa
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingServerSocketFactory.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 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.io.IOException;
+import java.net.InetAddress;
+import java.net.ServerSocket;
+import javax.net.ServerSocketFactory;
+
+/**
+ * A {@link ServerSocketFactory} that delegates calls. Sockets can be configured after creation by
+ * overriding {@link #configureServerSocket(java.net.ServerSocket)}.
+ */
+public class DelegatingServerSocketFactory extends ServerSocketFactory {
+
+ private final ServerSocketFactory delegate;
+
+ public DelegatingServerSocketFactory(ServerSocketFactory delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public ServerSocket createServerSocket() throws IOException {
+ ServerSocket serverSocket = delegate.createServerSocket();
+ configureServerSocket(serverSocket);
+ return serverSocket;
+ }
+
+ @Override
+ public ServerSocket createServerSocket(int port) throws IOException {
+ ServerSocket serverSocket = delegate.createServerSocket(port);
+ configureServerSocket(serverSocket);
+ return serverSocket;
+ }
+
+ @Override
+ public ServerSocket createServerSocket(int port, int backlog) throws IOException {
+ ServerSocket serverSocket = delegate.createServerSocket(port, backlog);
+ configureServerSocket(serverSocket);
+ return serverSocket;
+ }
+
+ @Override
+ public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress)
+ throws IOException {
+ ServerSocket serverSocket = delegate.createServerSocket(port, backlog, ifAddress);
+ configureServerSocket(serverSocket);
+ return serverSocket;
+ }
+
+ protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+ // No-op by default.
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java
new file mode 100644
index 0000000..e8fdfe8
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSocketFactory.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2014 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.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import javax.net.SocketFactory;
+
+/**
+ * A {@link SocketFactory} that delegates calls. Sockets can be configured after creation by
+ * overriding {@link #configureSocket(java.net.Socket)}.
+ */
+public class DelegatingSocketFactory extends SocketFactory {
+
+ private final SocketFactory delegate;
+
+ public DelegatingSocketFactory(SocketFactory delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ Socket socket = delegate.createSocket();
+ configureSocket(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
+ Socket socket = delegate.createSocket(host, port);
+ configureSocket(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localAddress, int localPort)
+ throws IOException, UnknownHostException {
+ Socket socket = delegate.createSocket(host, port, localAddress, localPort);
+ configureSocket(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ Socket socket = delegate.createSocket(host, port);
+ configureSocket(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort)
+ throws IOException {
+ Socket socket = delegate.createSocket(host, port, localAddress, localPort);
+ configureSocket(socket);
+ return socket;
+ }
+
+ protected void configureSocket(Socket socket) throws IOException {
+ // No-op by default.
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java
index a42362f..3d7701f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DispatcherTest.java
@@ -1,5 +1,6 @@
package com.squareup.okhttp;
+import com.squareup.okhttp.Call.AsyncCall;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@@ -14,7 +15,7 @@
public final class DispatcherTest {
RecordingExecutor executor = new RecordingExecutor();
- RecordingReceiver receiver = new RecordingReceiver();
+ RecordingCallback callback = new RecordingCallback();
Dispatcher dispatcher = new Dispatcher(executor);
OkHttpClient client = new OkHttpClient().setDispatcher(dispatcher);
@@ -40,53 +41,53 @@
}
@Test public void enqueuedJobsRunImmediately() throws Exception {
- client.enqueue(newRequest("http://a/1"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
executor.assertJobs("http://a/1");
}
@Test public void maxRequestsEnforced() throws Exception {
dispatcher.setMaxRequests(3);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
- client.enqueue(newRequest("http://b/1"), receiver);
- client.enqueue(newRequest("http://b/2"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
+ client.newCall(newRequest("http://b/1")).enqueue(callback);
+ client.newCall(newRequest("http://b/2")).enqueue(callback);
executor.assertJobs("http://a/1", "http://a/2", "http://b/1");
}
@Test public void maxPerHostEnforced() throws Exception {
dispatcher.setMaxRequestsPerHost(2);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
- client.enqueue(newRequest("http://a/3"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
+ client.newCall(newRequest("http://a/3")).enqueue(callback);
executor.assertJobs("http://a/1", "http://a/2");
}
@Test public void increasingMaxRequestsPromotesJobsImmediately() throws Exception {
dispatcher.setMaxRequests(2);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://b/1"), receiver);
- client.enqueue(newRequest("http://c/1"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
- client.enqueue(newRequest("http://b/2"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://b/1")).enqueue(callback);
+ client.newCall(newRequest("http://c/1")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
+ client.newCall(newRequest("http://b/2")).enqueue(callback);
dispatcher.setMaxRequests(4);
executor.assertJobs("http://a/1", "http://b/1", "http://c/1", "http://a/2");
}
@Test public void increasingMaxPerHostPromotesJobsImmediately() throws Exception {
dispatcher.setMaxRequestsPerHost(2);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
- client.enqueue(newRequest("http://a/3"), receiver);
- client.enqueue(newRequest("http://a/4"), receiver);
- client.enqueue(newRequest("http://a/5"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
+ client.newCall(newRequest("http://a/3")).enqueue(callback);
+ client.newCall(newRequest("http://a/4")).enqueue(callback);
+ client.newCall(newRequest("http://a/5")).enqueue(callback);
dispatcher.setMaxRequestsPerHost(4);
executor.assertJobs("http://a/1", "http://a/2", "http://a/3", "http://a/4");
}
@Test public void oldJobFinishesNewJobCanRunDifferentHost() throws Exception {
dispatcher.setMaxRequests(1);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://b/1"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://b/1")).enqueue(callback);
executor.finishJob("http://a/1");
executor.assertJobs("http://b/1");
}
@@ -94,36 +95,27 @@
@Test public void oldJobFinishesNewJobWithSameHostStarts() throws Exception {
dispatcher.setMaxRequests(2);
dispatcher.setMaxRequestsPerHost(1);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://b/1"), receiver);
- client.enqueue(newRequest("http://b/2"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://b/1")).enqueue(callback);
+ client.newCall(newRequest("http://b/2")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
executor.finishJob("http://a/1");
executor.assertJobs("http://b/1", "http://a/2");
}
@Test public void oldJobFinishesNewJobCantRunDueToHostLimit() throws Exception {
dispatcher.setMaxRequestsPerHost(1);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://b/1"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
+ client.newCall(newRequest("http://a/1")).enqueue(callback);
+ client.newCall(newRequest("http://b/1")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
executor.finishJob("http://b/1");
executor.assertJobs("http://a/1");
}
- @Test public void cancelingReadyJobPreventsItFromStarting() throws Exception {
- dispatcher.setMaxRequestsPerHost(1);
- client.enqueue(newRequest("http://a/1"), receiver);
- client.enqueue(newRequest("http://a/2", "tag1"), receiver);
- dispatcher.cancel("tag1");
- executor.finishJob("http://a/1");
- executor.assertJobs();
- }
-
@Test public void cancelingRunningJobTakesNoEffectUntilJobFinishes() throws Exception {
dispatcher.setMaxRequests(1);
- client.enqueue(newRequest("http://a/1", "tag1"), receiver);
- client.enqueue(newRequest("http://a/2"), receiver);
+ client.newCall(newRequest("http://a/1", "tag1")).enqueue(callback);
+ client.newCall(newRequest("http://a/2")).enqueue(callback);
dispatcher.cancel("tag1");
executor.assertJobs("http://a/1");
executor.finishJob("http://a/1");
@@ -131,26 +123,26 @@
}
class RecordingExecutor extends AbstractExecutorService {
- private List<Job> jobs = new ArrayList<Job>();
+ private List<AsyncCall> calls = new ArrayList<>();
@Override public void execute(Runnable command) {
- jobs.add((Job) command);
+ calls.add((AsyncCall) command);
}
public void assertJobs(String... expectedUrls) {
- List<String> actualUrls = new ArrayList<String>();
- for (Job job : jobs) {
- actualUrls.add(job.request().urlString());
+ List<String> actualUrls = new ArrayList<>();
+ for (AsyncCall call : calls) {
+ actualUrls.add(call.request().urlString());
}
assertEquals(Arrays.asList(expectedUrls), actualUrls);
}
public void finishJob(String url) {
- for (Iterator<Job> i = jobs.iterator(); i.hasNext(); ) {
- Job job = i.next();
- if (job.request().urlString().equals(url)) {
+ for (Iterator<AsyncCall> i = calls.iterator(); i.hasNext(); ) {
+ AsyncCall call = i.next();
+ if (call.request().urlString().equals(url)) {
i.remove();
- dispatcher.finished(job);
+ dispatcher.finished(call);
return;
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java
index 956a762..5f9e623 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/FallbackTestClientSocketFactory.java
@@ -24,11 +24,10 @@
import javax.net.ssl.SSLSocketFactory;
/**
- * An SSLSocketFactory that delegates calls. It keeps a record of any sockets created.
- * If {@link #disableTlsFallbackScsv} is set to {@code true} then sockets created by the delegate
- * are wrapped with ones that will not accept the {@link #TLS_FALLBACK_SCSV} cipher, thus
- * bypassing server-side fallback checks on platforms that support it. Unfortunately this wrapping
- * will disable any reflection-based calls to SSLSocket from Platform.
+ * An SSLSocketFactory that delegates calls. Sockets created by the delegate are wrapped with ones
+ * that will not accept the {@link #TLS_FALLBACK_SCSV} cipher, thus bypassing server-side fallback
+ * checks on platforms that support it. Unfortunately this wrapping will disable any
+ * reflection-based calls to SSLSocket from Platform.
*/
public class FallbackTestClientSocketFactory extends DelegatingSSLSocketFactory {
/**
@@ -37,79 +36,46 @@
*/
public static final String TLS_FALLBACK_SCSV = "TLS_FALLBACK_SCSV";
- private final boolean disableTlsFallbackScsv;
- private final List<SSLSocket> createdSockets = new ArrayList<SSLSocket>();
-
- public FallbackTestClientSocketFactory(SSLSocketFactory delegate,
- boolean disableTlsFallbackScsv) {
+ public FallbackTestClientSocketFactory(SSLSocketFactory delegate) {
super(delegate);
- this.disableTlsFallbackScsv = disableTlsFallbackScsv;
}
@Override public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
throws IOException {
SSLSocket socket = super.createSocket(s, host, port, autoClose);
- if (disableTlsFallbackScsv) {
- socket = new TlsFallbackDisabledScsvSSLSocket(socket);
- }
- createdSockets.add(socket);
- return socket;
+ return new TlsFallbackScsvDisabledSSLSocket(socket);
}
@Override public SSLSocket createSocket() throws IOException {
SSLSocket socket = super.createSocket();
- if (disableTlsFallbackScsv) {
- socket = new TlsFallbackDisabledScsvSSLSocket(socket);
- }
- createdSockets.add(socket);
- return socket;
+ return new TlsFallbackScsvDisabledSSLSocket(socket);
}
@Override public SSLSocket createSocket(String host,int port) throws IOException {
SSLSocket socket = super.createSocket(host, port);
- if (disableTlsFallbackScsv) {
- socket = new TlsFallbackDisabledScsvSSLSocket(socket);
- }
- createdSockets.add(socket);
- return socket;
+ return new TlsFallbackScsvDisabledSSLSocket(socket);
}
@Override public SSLSocket createSocket(String host,int port, InetAddress localHost,
int localPort) throws IOException {
SSLSocket socket = super.createSocket(host, port, localHost, localPort);
- if (disableTlsFallbackScsv) {
- socket = new TlsFallbackDisabledScsvSSLSocket(socket);
- }
- createdSockets.add(socket);
- return socket;
+ return new TlsFallbackScsvDisabledSSLSocket(socket);
}
@Override public SSLSocket createSocket(InetAddress host,int port) throws IOException {
SSLSocket socket = super.createSocket(host, port);
- if (disableTlsFallbackScsv) {
- socket = new TlsFallbackDisabledScsvSSLSocket(socket);
- }
- createdSockets.add(socket);
- return socket;
+ return new TlsFallbackScsvDisabledSSLSocket(socket);
}
@Override public SSLSocket createSocket(InetAddress address,int port,
InetAddress localAddress, int localPort) throws IOException {
SSLSocket socket = super.createSocket(address, port, localAddress, localPort);
- if (disableTlsFallbackScsv) {
- socket = new TlsFallbackDisabledScsvSSLSocket(socket);
- }
- createdSockets.add(socket);
- return socket;
+ return new TlsFallbackScsvDisabledSSLSocket(socket);
}
- public List<SSLSocket> getCreatedSockets() {
- return createdSockets;
- }
+ private static class TlsFallbackScsvDisabledSSLSocket extends DelegatingSSLSocket {
- private static class TlsFallbackDisabledScsvSSLSocket extends DelegatingSSLSocket {
-
- public TlsFallbackDisabledScsvSSLSocket(SSLSocket socket) {
+ public TlsFallbackScsvDisabledSSLSocket(SSLSocket socket) {
super(socket);
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java
new file mode 100644
index 0000000..a9533bf
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/FormEncodingBuilderTest.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2014 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 okio.Buffer;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public final class FormEncodingBuilderTest {
+ @Test public void urlEncoding() throws Exception {
+ RequestBody formEncoding = new FormEncodingBuilder()
+ .add("a&b", "c=d")
+ .add("space, the", "final frontier")
+ .build();
+
+ assertEquals("application/x-www-form-urlencoded", formEncoding.contentType().toString());
+
+ String expected = "a%26b=c%3Dd&space%2C+the=final+frontier";
+ assertEquals(expected.length(), formEncoding.contentLength());
+
+ Buffer out = new Buffer();
+ formEncoding.writeTo(out);
+ assertEquals(expected, out.readUtf8());
+ }
+
+ @Test public void encodedPair() throws Exception {
+ RequestBody formEncoding = new FormEncodingBuilder()
+ .add("sim", "ple")
+ .build();
+
+ String expected = "sim=ple";
+ assertEquals(expected.length(), formEncoding.contentLength());
+
+ Buffer buffer = new Buffer();
+ formEncoding.writeTo(buffer);
+ assertEquals(expected, buffer.readUtf8());
+ }
+
+ @Test public void encodeMultiplePairs() throws Exception {
+ RequestBody formEncoding = new FormEncodingBuilder()
+ .add("sim", "ple")
+ .add("hey", "there")
+ .add("help", "me")
+ .build();
+
+ String expected = "sim=ple&hey=there&help=me";
+ assertEquals(expected.length(), formEncoding.contentLength());
+
+ Buffer buffer = new Buffer();
+ formEncoding.writeTo(buffer);
+ assertEquals(expected, buffer.readUtf8());
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
new file mode 100644
index 0000000..8d16b07
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
@@ -0,0 +1,424 @@
+/*
+ * Copyright (C) 2014 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.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ForwardingSink;
+import okio.ForwardingSource;
+import okio.GzipSink;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.fail;
+
+public final class InterceptorTest {
+ @Rule public MockWebServerRule server = new MockWebServerRule();
+
+ private OkHttpClient client = new OkHttpClient();
+ private RecordingCallback callback = new RecordingCallback();
+
+ @Test public void applicationInterceptorsCanShortCircuitResponses() throws Exception {
+ server.get().shutdown(); // Accept no connections.
+
+ Request request = new Request.Builder()
+ .url("https://localhost:1/")
+ .build();
+
+ final Response interceptorResponse = new Response.Builder()
+ .request(request)
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Intercepted!")
+ .body(ResponseBody.create(MediaType.parse("text/plain; charset=utf-8"), "abc"))
+ .build();
+
+ client.interceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ return interceptorResponse;
+ }
+ });
+
+ Response response = client.newCall(request).execute();
+ assertSame(interceptorResponse, response);
+ }
+
+ @Test public void networkInterceptorsCannotShortCircuitResponses() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(500));
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ return new Response.Builder()
+ .request(chain.request())
+ .protocol(Protocol.HTTP_1_1)
+ .code(200)
+ .message("Intercepted!")
+ .body(ResponseBody.create(MediaType.parse("text/plain; charset=utf-8"), "abc"))
+ .build();
+ }
+ };
+ client.networkInterceptors().add(interceptor);
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("network interceptor " + interceptor + " must call proceed() exactly once",
+ expected.getMessage());
+ }
+ }
+
+ @Test public void networkInterceptorsCannotCallProceedMultipleTimes() throws Exception {
+ server.enqueue(new MockResponse());
+ server.enqueue(new MockResponse());
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ chain.proceed(chain.request());
+ return chain.proceed(chain.request());
+ }
+ };
+ client.networkInterceptors().add(interceptor);
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("network interceptor " + interceptor + " must call proceed() exactly once",
+ expected.getMessage());
+ }
+ }
+
+ @Test public void networkInterceptorsCannotChangeServerAddress() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(500));
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Address address = chain.connection().getRoute().getAddress();
+ String sameHost = address.getUriHost();
+ int differentPort = address.getUriPort() + 1;
+ return chain.proceed(chain.request().newBuilder()
+ .url(new URL("http://" + sameHost + ":" + differentPort + "/"))
+ .build());
+ }
+ };
+ client.networkInterceptors().add(interceptor);
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("network interceptor " + interceptor + " must retain the same host and port",
+ expected.getMessage());
+ }
+ }
+
+ @Test public void networkInterceptorsHaveConnectionAccess() throws Exception {
+ server.enqueue(new MockResponse());
+
+ client.networkInterceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Connection connection = chain.connection();
+ assertNotNull(connection);
+ return chain.proceed(chain.request());
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request).execute();
+ }
+
+ @Test public void networkInterceptorsObserveNetworkHeaders() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody(gzip("abcabcabc"))
+ .addHeader("Content-Encoding: gzip"));
+
+ client.networkInterceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ // The network request has everything: User-Agent, Host, Accept-Encoding.
+ Request networkRequest = chain.request();
+ assertNotNull(networkRequest.header("User-Agent"));
+ assertEquals(server.get().getHostName() + ":" + server.get().getPort(),
+ networkRequest.header("Host"));
+ assertNotNull(networkRequest.header("Accept-Encoding"));
+
+ // The network response also has everything, including the raw gzipped content.
+ Response networkResponse = chain.proceed(networkRequest);
+ assertEquals("gzip", networkResponse.header("Content-Encoding"));
+ return networkResponse;
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ // No extra headers in the application's request.
+ assertNull(request.header("User-Agent"));
+ assertNull(request.header("Host"));
+ assertNull(request.header("Accept-Encoding"));
+
+ // No extra headers in the application's response.
+ Response response = client.newCall(request).execute();
+ assertNull(request.header("Content-Encoding"));
+ assertEquals("abcabcabc", response.body().string());
+ }
+
+ @Test public void applicationInterceptorsRewriteRequestToServer() throws Exception {
+ rewriteRequestToServer(client.interceptors());
+ }
+
+ @Test public void networkInterceptorsRewriteRequestToServer() throws Exception {
+ rewriteRequestToServer(client.networkInterceptors());
+ }
+
+ private void rewriteRequestToServer(List<Interceptor> interceptors) throws Exception {
+ server.enqueue(new MockResponse());
+
+ interceptors.add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ return chain.proceed(originalRequest.newBuilder()
+ .method("POST", uppercase(originalRequest.body()))
+ .addHeader("OkHttp-Intercepted", "yep")
+ .build());
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .addHeader("Original-Header", "foo")
+ .method("PUT", RequestBody.create(MediaType.parse("text/plain"), "abc"))
+ .build();
+
+ client.newCall(request).execute();
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("ABC", recordedRequest.getBody().readUtf8());
+ assertEquals("foo", recordedRequest.getHeader("Original-Header"));
+ assertEquals("yep", recordedRequest.getHeader("OkHttp-Intercepted"));
+ assertEquals("POST", recordedRequest.getMethod());
+ }
+
+ @Test public void applicationInterceptorsRewriteResponseFromServer() throws Exception {
+ rewriteResponseFromServer(client.interceptors());
+ }
+
+ @Test public void networkInterceptorsRewriteResponseFromServer() throws Exception {
+ rewriteResponseFromServer(client.networkInterceptors());
+ }
+
+ private void rewriteResponseFromServer(List<Interceptor> interceptors) throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Original-Header: foo")
+ .setBody("abc"));
+
+ interceptors.add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Response originalResponse = chain.proceed(chain.request());
+ return originalResponse.newBuilder()
+ .body(uppercase(originalResponse.body()))
+ .addHeader("OkHttp-Intercepted", "yep")
+ .build();
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ Response response = client.newCall(request).execute();
+ assertEquals("ABC", response.body().string());
+ assertEquals("yep", response.header("OkHttp-Intercepted"));
+ assertEquals("foo", response.header("Original-Header"));
+ }
+
+ @Test public void multipleApplicationInterceptors() throws Exception {
+ multipleInterceptors(client.interceptors());
+ }
+
+ @Test public void multipleNetworkInterceptors() throws Exception {
+ multipleInterceptors(client.networkInterceptors());
+ }
+
+ private void multipleInterceptors(List<Interceptor> interceptors) throws Exception {
+ server.enqueue(new MockResponse());
+
+ interceptors.add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ Response originalResponse = chain.proceed(originalRequest.newBuilder()
+ .addHeader("Request-Interceptor", "Android") // 1. Added first.
+ .build());
+ return originalResponse.newBuilder()
+ .addHeader("Response-Interceptor", "Donut") // 4. Added last.
+ .build();
+ }
+ });
+ interceptors.add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ Response originalResponse = chain.proceed(originalRequest.newBuilder()
+ .addHeader("Request-Interceptor", "Bob") // 2. Added second.
+ .build());
+ return originalResponse.newBuilder()
+ .addHeader("Response-Interceptor", "Cupcake") // 3. Added third.
+ .build();
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ Response response = client.newCall(request).execute();
+ assertEquals(Arrays.asList("Cupcake", "Donut"),
+ response.headers("Response-Interceptor"));
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals(Arrays.asList("Android", "Bob"),
+ recordedRequest.getHeaders().values("Request-Interceptor"));
+ }
+
+ @Test public void asyncApplicationInterceptors() throws Exception {
+ asyncInterceptors(client.interceptors());
+ }
+
+ @Test public void asyncNetworkInterceptors() throws Exception {
+ asyncInterceptors(client.networkInterceptors());
+ }
+
+ private void asyncInterceptors(List<Interceptor> interceptors) throws Exception {
+ server.enqueue(new MockResponse());
+
+ interceptors.add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Response originalResponse = chain.proceed(chain.request());
+ return originalResponse.newBuilder()
+ .addHeader("OkHttp-Intercepted", "yep")
+ .build();
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+ client.newCall(request).enqueue(callback);
+
+ callback.await(request.url())
+ .assertCode(200)
+ .assertHeader("OkHttp-Intercepted", "yep");
+ }
+
+ @Test public void applicationInterceptorsCanMakeMultipleRequestsToServer() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ client.interceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ chain.proceed(chain.request());
+ return chain.proceed(chain.request());
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.getUrl("/"))
+ .build();
+
+ Response response = client.newCall(request).execute();
+ assertEquals(response.body().string(), "b");
+ }
+
+ private RequestBody uppercase(final RequestBody original) {
+ return new RequestBody() {
+ @Override public MediaType contentType() {
+ return original.contentType();
+ }
+
+ @Override public long contentLength() throws IOException {
+ return original.contentLength();
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ Sink uppercase = uppercase(sink);
+ BufferedSink bufferedSink = Okio.buffer(uppercase);
+ original.writeTo(bufferedSink);
+ bufferedSink.emit();
+ }
+ };
+ }
+
+ private Sink uppercase(final BufferedSink original) {
+ return new ForwardingSink(original) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ original.writeUtf8(source.readUtf8(byteCount).toUpperCase(Locale.US));
+ }
+ };
+ }
+
+ static ResponseBody uppercase(ResponseBody original) throws IOException {
+ return ResponseBody.create(original.contentType(), original.contentLength(),
+ Okio.buffer(uppercase(original.source())));
+ }
+
+ private static Source uppercase(final Source original) {
+ return new ForwardingSource(original) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ Buffer mixedCase = new Buffer();
+ long count = original.read(mixedCase, byteCount);
+ sink.writeUtf8(mixedCase.readUtf8().toUpperCase(Locale.US));
+ return count;
+ }
+ };
+ }
+
+ private Buffer gzip(String data) throws IOException {
+ Buffer result = new Buffer();
+ BufferedSink sink = Okio.buffer(new GzipSink(result));
+ sink.writeUtf8(data);
+ sink.close();
+ return result;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/LimitedProtocolsSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/LimitedProtocolsSocketFactory.java
deleted file mode 100644
index 58ce111..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/LimitedProtocolsSocketFactory.java
+++ /dev/null
@@ -1,82 +0,0 @@
-/*
- * Copyright 2014 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.io.IOException;
-import java.net.InetAddress;
-import java.net.Socket;
-import java.net.UnknownHostException;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-
-/**
- * An {@link SSLSocketFactory} that creates sockets using a delegate, but overrides the enabled
- * protocols for any created sockets.
- */
-public class LimitedProtocolsSocketFactory extends DelegatingSSLSocketFactory {
-
- private final String[] enabledProtocols;
-
- public LimitedProtocolsSocketFactory(SSLSocketFactory delegate, String... enabledProtocols) {
- super(delegate);
- this.enabledProtocols = enabledProtocols;
- }
-
- @Override
- public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose)
- throws IOException {
- SSLSocket socket = super.createSocket(s, host, port, autoClose);
- socket.setEnabledProtocols(enabledProtocols);
- return socket;
- }
-
- @Override
- public SSLSocket createSocket() throws IOException {
- SSLSocket socket = super.createSocket();
- socket.setEnabledProtocols(enabledProtocols);
- return socket;
- }
-
- @Override
- public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
- SSLSocket socket = super.createSocket(host, port);
- socket.setEnabledProtocols(enabledProtocols);
- return socket;
- }
-
- @Override
- public SSLSocket createSocket(String host, int port, InetAddress localHost, int localPort)
- throws IOException, UnknownHostException {
- SSLSocket socket = super.createSocket(host, port, localHost, localPort);
- socket.setEnabledProtocols(enabledProtocols);
- return socket;
- }
-
- @Override
- public SSLSocket createSocket(InetAddress host, int port) throws IOException {
- SSLSocket socket = super.createSocket(host, port);
- socket.setEnabledProtocols(enabledProtocols);
- return socket;
- }
-
- @Override
- public SSLSocket createSocket(InetAddress address, int port, InetAddress localAddress,
- int localPort) throws IOException {
- SSLSocket socket = super.createSocket(address, port, localAddress, localPort);
- socket.setEnabledProtocols(enabledProtocols);
- return socket;
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/MediaTypeTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/MediaTypeTest.java
index acbfdd5..2580595 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/MediaTypeTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/MediaTypeTest.java
@@ -54,6 +54,13 @@
assertMediaType("text/plain; a=1; a=2; b=3");
assertMediaType("text/plain; charset=utf-16");
assertMediaType("text/plain; \t \n \r a=b");
+ assertMediaType("text/plain;");
+ assertMediaType("text/plain; ");
+ assertMediaType("text/plain; a=1;");
+ assertMediaType("text/plain; a=1; ");
+ assertMediaType("text/plain; a=1;; b=2");
+ assertMediaType("text/plain;;");
+ assertMediaType("text/plain; ;");
}
@Test public void testInvalidParse() throws Exception {
@@ -64,14 +71,10 @@
assertInvalid("text/");
assertInvalid("te<t/plain");
assertInvalid("text/pl@in");
- assertInvalid("text/plain;");
- assertInvalid("text/plain; ");
assertInvalid("text/plain; a");
assertInvalid("text/plain; a=");
assertInvalid("text/plain; a=@");
assertInvalid("text/plain; a=\"@");
- assertInvalid("text/plain; a=1;");
- assertInvalid("text/plain; a=1; ");
assertInvalid("text/plain; a=1; b");
assertInvalid("text/plain; a=1; b=");
assertInvalid("text/plain; a=\u2025");
@@ -104,6 +107,11 @@
assertEquals("UTF-8", mediaType.charset().name());
}
+ @Test public void testDuplicatedCharsets() {
+ MediaType mediaType = MediaType.parse("text/plain; charset=utf-8; charset=UTF-8");
+ assertEquals("UTF-8", mediaType.charset().name());
+ }
+
@Test public void testMultipleCharsets() {
try {
MediaType.parse("text/plain; charset=utf-8; charset=utf-16");
@@ -140,6 +148,14 @@
assertEquals("ISO-8859-1", charset.charset(Charset.forName("US-ASCII")).name());
}
+ @Test public void testParseDanglingSemicolon() throws Exception {
+ MediaType mediaType = MediaType.parse("text/plain;");
+ assertEquals("text", mediaType.type());
+ assertEquals("plain", mediaType.subtype());
+ assertEquals(null, mediaType.charset());
+ assertEquals("text/plain;", mediaType.toString());
+ }
+
private void assertMediaType(String string) {
MediaType mediaType = MediaType.parse(string);
assertEquals(string, mediaType.toString());
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/MultipartBuilderTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/MultipartBuilderTest.java
new file mode 100644
index 0000000..0e12470
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/MultipartBuilderTest.java
@@ -0,0 +1,241 @@
+/*
+ * Copyright (C) 2014 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.io.IOException;
+import okio.Buffer;
+import okio.BufferedSink;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class MultipartBuilderTest {
+ @Test(expected = IllegalStateException.class)
+ public void onePartRequired() throws Exception {
+ new MultipartBuilder().build();
+ }
+
+ @Test public void singlePart() throws Exception {
+ String expected = ""
+ + "--123\r\n"
+ + "Content-Length: 13\r\n"
+ + "\r\n"
+ + "Hello, World!\r\n"
+ + "--123--\r\n";
+
+ RequestBody requestBody = new MultipartBuilder("123")
+ .addPart(RequestBody.create(null, "Hello, World!"))
+ .build();
+
+ assertEquals("multipart/mixed; boundary=123", requestBody.contentType().toString());
+
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ assertEquals(-1L, requestBody.contentLength());
+ assertEquals(expected, buffer.readUtf8());
+ }
+
+ @Test public void threeParts() throws Exception {
+ String expected = ""
+ + "--123\r\n"
+ + "Content-Length: 5\r\n"
+ + "\r\n"
+ + "Quick\r\n"
+ + "--123\r\n"
+ + "Content-Length: 5\r\n"
+ + "\r\n"
+ + "Brown\r\n"
+ + "--123\r\n"
+ + "Content-Length: 3\r\n"
+ + "\r\n"
+ + "Fox\r\n"
+ + "--123--\r\n";
+
+ RequestBody requestBody = new MultipartBuilder("123")
+ .addPart(RequestBody.create(null, "Quick"))
+ .addPart(RequestBody.create(null, "Brown"))
+ .addPart(RequestBody.create(null, "Fox"))
+ .build();
+
+ assertEquals("multipart/mixed; boundary=123", requestBody.contentType().toString());
+
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ assertEquals(-1L, requestBody.contentLength());
+ assertEquals(expected, buffer.readUtf8());
+ }
+
+ @Test public void fieldAndTwoFiles() throws Exception {
+ String expected = ""
+ + "--AaB03x\r\n"
+ + "Content-Disposition: form-data; name=\"submit-name\"\r\n"
+ + "Content-Length: 5\r\n"
+ + "\r\n"
+ + "Larry\r\n"
+ + "--AaB03x\r\n"
+ + "Content-Disposition: form-data; name=\"files\"\r\n"
+ + "Content-Type: multipart/mixed; boundary=BbC04y\r\n"
+ + "\r\n"
+ + "--BbC04y\r\n"
+ + "Content-Disposition: file; filename=\"file1.txt\"\r\n"
+ + "Content-Type: text/plain; charset=utf-8\r\n"
+ + "Content-Length: 29\r\n"
+ + "\r\n"
+ + "... contents of file1.txt ...\r\n"
+ + "--BbC04y\r\n"
+ + "Content-Disposition: file; filename=\"file2.gif\"\r\n"
+ + "Content-Transfer-Encoding: binary\r\n"
+ + "Content-Type: image/gif\r\n"
+ + "Content-Length: 29\r\n"
+ + "\r\n"
+ + "... contents of file2.gif ...\r\n"
+ + "--BbC04y--\r\n"
+ + "\r\n"
+ + "--AaB03x--\r\n";
+
+ RequestBody requestBody = new MultipartBuilder("AaB03x")
+ .type(MultipartBuilder.FORM)
+ .addFormDataPart("submit-name", "Larry")
+ .addFormDataPart("files", null,
+ new MultipartBuilder("BbC04y")
+ .addPart(
+ Headers.of("Content-Disposition", "file; filename=\"file1.txt\""),
+ RequestBody.create(
+ MediaType.parse("text/plain"), "... contents of file1.txt ..."))
+ .addPart(
+ Headers.of(
+ "Content-Disposition", "file; filename=\"file2.gif\"",
+ "Content-Transfer-Encoding", "binary"),
+ RequestBody.create(
+ MediaType.parse("image/gif"),
+ "... contents of file2.gif ...".getBytes(UTF_8)))
+ .build())
+ .build();
+
+ assertEquals("multipart/form-data; boundary=AaB03x", requestBody.contentType().toString());
+
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ assertEquals(-1L, requestBody.contentLength());
+ assertEquals(expected, buffer.readUtf8());
+ }
+
+ @Test public void stringEscapingIsWeird() throws Exception {
+ String expected = ""
+ + "--AaB03x\r\n"
+ + "Content-Disposition: form-data; name=\"field with spaces\"; filename=\"filename with spaces.txt\"\r\n"
+ + "Content-Type: text/plain; charset=utf-8\r\n"
+ + "Content-Length: 4\r\n"
+ + "\r\n"
+ + "okay\r\n"
+ + "--AaB03x\r\n"
+ + "Content-Disposition: form-data; name=\"field with %22\"\r\n"
+ + "Content-Length: 1\r\n"
+ + "\r\n"
+ + "\"\r\n"
+ + "--AaB03x\r\n"
+ + "Content-Disposition: form-data; name=\"field with %22\"\r\n"
+ + "Content-Length: 3\r\n"
+ + "\r\n"
+ + "%22\r\n"
+ + "--AaB03x\r\n"
+ + "Content-Disposition: form-data; name=\"field with \u0391\"\r\n"
+ + "Content-Length: 5\r\n"
+ + "\r\n"
+ + "Alpha\r\n"
+ + "--AaB03x--\r\n";
+
+ RequestBody requestBody = new MultipartBuilder("AaB03x")
+ .type(MultipartBuilder.FORM)
+ .addFormDataPart("field with spaces", "filename with spaces.txt",
+ RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), "okay"))
+ .addFormDataPart("field with \"", "\"")
+ .addFormDataPart("field with %22", "%22")
+ .addFormDataPart("field with \u0391", "Alpha")
+ .build();
+
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ assertEquals(expected, buffer.readUtf8());
+ }
+
+ @Test public void streamingPartHasNoLength() throws Exception {
+ class StreamingBody extends RequestBody {
+ private final String body;
+
+ StreamingBody(String body) {
+ this.body = body;
+ }
+
+ @Override public MediaType contentType() {
+ return null;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8(body);
+ }
+ }
+
+ String expected = ""
+ + "--123\r\n"
+ + "Content-Length: 5\r\n"
+ + "\r\n"
+ + "Quick\r\n"
+ + "--123\r\n"
+ + "\r\n"
+ + "Brown\r\n"
+ + "--123\r\n"
+ + "Content-Length: 3\r\n"
+ + "\r\n"
+ + "Fox\r\n"
+ + "--123--\r\n";
+
+ RequestBody requestBody = new MultipartBuilder("123")
+ .addPart(RequestBody.create(null, "Quick"))
+ .addPart(new StreamingBody("Brown"))
+ .addPart(RequestBody.create(null, "Fox"))
+ .build();
+
+ assertEquals("multipart/mixed; boundary=123", requestBody.contentType().toString());
+
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+ assertEquals(expected, buffer.readUtf8());
+ assertEquals(-1, requestBody.contentLength());
+ }
+
+ @Test public void contentTypeHeaderIsForbidden() throws Exception {
+ try {
+ new MultipartBuilder().addPart(
+ Headers.of("Content-Type", "text/plain"),
+ RequestBody.create(null, "Hello, World!"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void contentLengthHeaderIsForbidden() throws Exception {
+ try {
+ new MultipartBuilder().addPart(
+ Headers.of("Content-Length", "13"),
+ RequestBody.create(null, "Hello, World!"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java
new file mode 100644
index 0000000..0bb8d1a
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/OkHttpClientTest.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2014 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.RecordingAuthenticator;
+import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
+import com.squareup.okhttp.internal.http.RecordingProxySelector;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
+import java.io.IOException;
+import java.net.Authenticator;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.ProxySelector;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import javax.net.SocketFactory;
+import org.junit.After;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class OkHttpClientTest {
+ private static final ProxySelector DEFAULT_PROXY_SELECTOR = ProxySelector.getDefault();
+ private static final CookieHandler DEFAULT_COOKIE_HANDLER = CookieManager.getDefault();
+ private static final ResponseCache DEFAULT_RESPONSE_CACHE = ResponseCache.getDefault();
+ private static final Authenticator DEFAULT_AUTHENTICATOR = null; // No Authenticator.getDefault().
+
+ @After public void tearDown() throws Exception {
+ ProxySelector.setDefault(DEFAULT_PROXY_SELECTOR);
+ CookieManager.setDefault(DEFAULT_COOKIE_HANDLER);
+ ResponseCache.setDefault(DEFAULT_RESPONSE_CACHE);
+ Authenticator.setDefault(DEFAULT_AUTHENTICATOR);
+ }
+
+ /** Confirm that {@code copyWithDefaults} gets expected constant values. */
+ @Test public void copyWithDefaultsWhenDefaultIsAConstant() throws Exception {
+ OkHttpClient client = new OkHttpClient().copyWithDefaults();
+ assertNull(client.internalCache());
+ assertEquals(0, client.getConnectTimeout());
+ assertEquals(0, client.getReadTimeout());
+ assertEquals(0, client.getWriteTimeout());
+ assertTrue(client.getFollowSslRedirects());
+ assertNull(client.getProxy());
+ assertEquals(Arrays.asList(Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1),
+ client.getProtocols());
+ }
+
+ /**
+ * Confirm that {@code copyWithDefaults} gets some default implementations
+ * from the core library.
+ */
+ @Test public void copyWithDefaultsWhenDefaultIsGlobal() throws Exception {
+ ProxySelector proxySelector = new RecordingProxySelector();
+ CookieManager cookieManager = new CookieManager();
+ Authenticator authenticator = new RecordingAuthenticator();
+ SocketFactory socketFactory = SocketFactory.getDefault(); // Global isn't configurable.
+ OkHostnameVerifier hostnameVerifier = OkHostnameVerifier.INSTANCE; // Global isn't configurable.
+ CertificatePinner certificatePinner = CertificatePinner.DEFAULT; // Global isn't configurable.
+
+ CookieManager.setDefault(cookieManager);
+ ProxySelector.setDefault(proxySelector);
+ Authenticator.setDefault(authenticator);
+
+ OkHttpClient client = new OkHttpClient().copyWithDefaults();
+
+ assertSame(proxySelector, client.getProxySelector());
+ assertSame(cookieManager, client.getCookieHandler());
+ assertSame(AuthenticatorAdapter.INSTANCE, client.getAuthenticator());
+ assertSame(socketFactory, client.getSocketFactory());
+ assertSame(hostnameVerifier, client.getHostnameVerifier());
+ assertSame(certificatePinner, client.getCertificatePinner());
+ }
+
+ /** There is no default cache. */
+ @Test public void copyWithDefaultsCacheIsNull() throws Exception {
+ OkHttpClient client = new OkHttpClient().copyWithDefaults();
+ assertNull(client.getCache());
+ }
+
+ @Test public void copyWithDefaultsDoesNotHonorGlobalResponseCache() {
+ ResponseCache.setDefault(new ResponseCache() {
+ @Override public CacheResponse get(URI uri, String requestMethod,
+ Map<String, List<String>> requestHeaders) throws IOException {
+ throw new AssertionError();
+ }
+
+ @Override public CacheRequest put(URI uri, URLConnection connection) {
+ throw new AssertionError();
+ }
+ });
+
+ OkHttpClient client = new OkHttpClient().copyWithDefaults();
+ assertNull(client.internalCache());
+ }
+
+ /**
+ * When copying the client, stateful things like the connection pool are
+ * shared across all clients.
+ */
+ @Test public void cloneSharesStatefulInstances() throws Exception {
+ OkHttpClient client = new OkHttpClient();
+
+ // Values should be non-null.
+ OkHttpClient a = client.clone().copyWithDefaults();
+ assertNotNull(a.routeDatabase());
+ assertNotNull(a.getDispatcher());
+ assertNotNull(a.getConnectionPool());
+ assertNotNull(a.getSslSocketFactory());
+
+ // Multiple clients share the instances.
+ OkHttpClient b = client.clone().copyWithDefaults();
+ assertSame(a.routeDatabase(), b.routeDatabase());
+ assertSame(a.getDispatcher(), b.getDispatcher());
+ assertSame(a.getConnectionPool(), b.getConnectionPool());
+ assertSame(a.getSslSocketFactory(), b.getSslSocketFactory());
+ }
+
+ /** We don't want to run user code inside of HttpEngine, etc. */
+ @Test public void copyWithDefaultsDoesNotReturnSubclass() throws Exception {
+ OkHttpClient subclass = new OkHttpClient() {};
+ OkHttpClient copy = subclass.copyWithDefaults();
+ assertEquals(OkHttpClient.class, copy.getClass());
+ }
+
+ @Test public void cloneReturnsSubclass() throws Exception {
+ OkHttpClient subclass = new OkHttpClient() {};
+ OkHttpClient clone = subclass.clone();
+ assertEquals(subclass.getClass(), clone.getClass());
+ }
+
+ /** Exercise a synchronous mocking case. */
+ @Test public void mock() throws Exception {
+ final Request request = new Request.Builder()
+ .url("http://example.com/")
+ .build();
+ final Response response = new Response.Builder()
+ .protocol(Protocol.HTTP_1_1)
+ .request(request)
+ .code(200)
+ .message("Alright")
+ .build();
+
+ OkHttpClient mockClient = new OkHttpClient() {
+ @Override public Call newCall(Request request) {
+ return new Call(this, request) {
+ @Override public Response execute() throws IOException {
+ return response;
+ }
+ @Override public void enqueue(Callback responseCallback) {
+ }
+ @Override public void cancel() {
+ }
+ };
+ }
+ };
+
+ Response actualResponse = mockClient.newCall(request).execute();
+ assertSame(response, actualResponse);
+ }
+
+ @Test public void setProtocolsRejectsHttp10() throws Exception {
+ OkHttpClient client = new OkHttpClient();
+ try {
+ client.setProtocols(Arrays.asList(Protocol.HTTP_1_0, Protocol.HTTP_1_1));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java
index 5b57baa..fdb1404 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordedResponse.java
@@ -15,45 +15,68 @@
*/
package com.squareup.okhttp;
-import java.util.ArrayList;
+import com.squareup.okhttp.internal.ws.WebSocket;
+import java.io.IOException;
+import java.net.URL;
import java.util.Arrays;
-import java.util.List;
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.fail;
+import static org.junit.Assert.assertTrue;
/**
* A received response or failure recorded by the response recorder.
*/
-public class RecordedResponse {
+public final class RecordedResponse {
public final Request request;
public final Response response;
+ public final WebSocket webSocket;
public final String body;
- public final Failure failure;
+ public final IOException failure;
- RecordedResponse(Request request, Response response, String body, Failure failure) {
+ public RecordedResponse(Request request, Response response, WebSocket webSocket, String body,
+ IOException failure) {
this.request = request;
this.response = response;
+ this.webSocket = webSocket;
this.body = body;
this.failure = failure;
}
+ public RecordedResponse assertRequestUrl(URL url) {
+ assertEquals(url, request.url());
+ return this;
+ }
+
+ public RecordedResponse assertRequestMethod(String method) {
+ assertEquals(method, request.method());
+ return this;
+ }
+
+ public RecordedResponse assertRequestHeader(String name, String... values) {
+ assertEquals(Arrays.asList(values), request.headers(name));
+ return this;
+ }
+
public RecordedResponse assertCode(int expectedCode) {
assertEquals(expectedCode, response.code());
return this;
}
- public RecordedResponse assertContainsHeaders(String... expectedHeaders) {
- List<String> actualHeaders = new ArrayList<String>();
- Headers headers = response.headers();
- for (int i = 0; i < headers.size(); i++) {
- actualHeaders.add(headers.name(i) + ": " + headers.value(i));
- }
- if (!actualHeaders.containsAll(Arrays.asList(expectedHeaders))) {
- fail("Expected: " + actualHeaders + "\nto contain: " + Arrays.toString(expectedHeaders));
- }
+ public RecordedResponse assertSuccessful() {
+ assertTrue(response.isSuccessful());
+ return this;
+ }
+
+ public RecordedResponse assertNotSuccessful() {
+ assertFalse(response.isSuccessful());
+ return this;
+ }
+
+ public RecordedResponse assertHeader(String name, String... values) {
+ assertEquals(Arrays.asList(values), response.headers(name));
return this;
}
@@ -73,18 +96,52 @@
}
/**
- * Asserts that the current response was redirected and returns a new recorded
- * response for the original request.
+ * Asserts that the current response was redirected and returns the prior
+ * response.
*/
- public RecordedResponse redirectedBy() {
- Response redirectedBy = response.priorResponse();
- assertNotNull(redirectedBy);
- assertNull(redirectedBy.body());
- return new RecordedResponse(redirectedBy.request(), redirectedBy, null, null);
+ public RecordedResponse priorResponse() {
+ Response priorResponse = response.priorResponse();
+ assertNotNull(priorResponse);
+ assertNull(priorResponse.body());
+ return new RecordedResponse(priorResponse.request(), priorResponse, null, null, null);
}
- public void assertFailure(String message) {
+ /**
+ * Asserts that the current response used the network and returns the network
+ * response.
+ */
+ public RecordedResponse networkResponse() {
+ Response networkResponse = response.networkResponse();
+ assertNotNull(networkResponse);
+ assertNull(networkResponse.body());
+ return new RecordedResponse(networkResponse.request(), networkResponse, null, null, null);
+ }
+
+ /** Asserts that the current response didn't use the network. */
+ public RecordedResponse assertNoNetworkResponse() {
+ assertNull(response.networkResponse());
+ return this;
+ }
+
+ /** Asserts that the current response didn't use the cache. */
+ public RecordedResponse assertNoCacheResponse() {
+ assertNull(response.cacheResponse());
+ return this;
+ }
+
+ /**
+ * Asserts that the current response used the cache and returns the cache
+ * response.
+ */
+ public RecordedResponse cacheResponse() {
+ Response cacheResponse = response.cacheResponse();
+ assertNotNull(cacheResponse);
+ assertNull(cacheResponse.body());
+ return new RecordedResponse(cacheResponse.request(), cacheResponse, null, null, null);
+ }
+
+ public void assertFailure(String... messages) {
assertNotNull(failure);
- assertEquals(message, failure.exception().getMessage());
+ assertTrue(failure.getMessage(), Arrays.asList(messages).contains(failure.getMessage()));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
similarity index 64%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
index 9bc8475..73e38f0 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingReceiver.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
@@ -15,56 +15,34 @@
*/
package com.squareup.okhttp;
-import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.TimeUnit;
+import okio.Buffer;
/**
* Records received HTTP responses so they can be later retrieved by tests.
*/
-public class RecordingReceiver implements Response.Receiver {
+public class RecordingCallback implements Callback {
public static final long TIMEOUT_MILLIS = TimeUnit.SECONDS.toMillis(10);
- private final Map<Response, ByteArrayOutputStream> inFlightResponses
- = new LinkedHashMap<Response, ByteArrayOutputStream>();
- private final List<RecordedResponse> responses = new ArrayList<RecordedResponse>();
+ private final List<RecordedResponse> responses = new ArrayList<>();
- @Override public synchronized void onFailure(Failure failure) {
- responses.add(new RecordedResponse(failure.request(), null, null, failure));
+ @Override public synchronized void onFailure(Request request, IOException e) {
+ responses.add(new RecordedResponse(request, null, null, null, e));
notifyAll();
}
- @Override public synchronized boolean onResponse(Response response) throws IOException {
- ByteArrayOutputStream out = inFlightResponses.get(response);
- if (out == null) {
- out = new ByteArrayOutputStream();
- inFlightResponses.put(response, out);
- }
+ @Override public synchronized void onResponse(Response response) throws IOException {
+ Buffer buffer = new Buffer();
+ ResponseBody body = response.body();
+ body.source().readAll(buffer);
- byte[] buffer = new byte[1024];
- Response.Body body = response.body();
-
- while (body.ready()) {
- int c = body.byteStream().read(buffer);
-
- if (c == -1) {
- inFlightResponses.remove(response);
- responses.add(new RecordedResponse(
- response.request(), response, out.toString("UTF-8"), null));
- notifyAll();
- return true;
- }
-
- out.write(buffer, 0, c);
- }
-
- return false;
+ responses.add(new RecordedResponse(response.request(), response, null, buffer.readUtf8(), null));
+ notifyAll();
}
/**
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
index 08f304e..8a74d1b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
@@ -19,15 +19,20 @@
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
-import okio.OkBuffer;
+import java.net.URI;
+import java.net.URL;
+import java.util.Arrays;
+import java.util.Collections;
+import okio.Buffer;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
public final class RequestTest {
@Test public void string() throws Exception {
MediaType contentType = MediaType.parse("text/plain; charset=utf-8");
- Request.Body body = Request.Body.create(contentType, "abc".getBytes(Util.UTF_8));
+ RequestBody body = RequestBody.create(contentType, "abc".getBytes(Util.UTF_8));
assertEquals(contentType, body.contentType());
assertEquals(3, body.contentLength());
assertEquals("616263", bodyToHex(body));
@@ -36,7 +41,7 @@
@Test public void stringWithDefaultCharsetAdded() throws Exception {
MediaType contentType = MediaType.parse("text/plain");
- Request.Body body = Request.Body.create(contentType, "\u0800");
+ RequestBody body = RequestBody.create(contentType, "\u0800");
assertEquals(MediaType.parse("text/plain; charset=utf-8"), body.contentType());
assertEquals(3, body.contentLength());
assertEquals("e0a080", bodyToHex(body));
@@ -44,7 +49,7 @@
@Test public void stringWithNonDefaultCharsetSpecified() throws Exception {
MediaType contentType = MediaType.parse("text/plain; charset=utf-16be");
- Request.Body body = Request.Body.create(contentType, "\u0800");
+ RequestBody body = RequestBody.create(contentType, "\u0800");
assertEquals(contentType, body.contentType());
assertEquals(2, body.contentLength());
assertEquals("0800", bodyToHex(body));
@@ -52,7 +57,7 @@
@Test public void byteArray() throws Exception {
MediaType contentType = MediaType.parse("text/plain");
- Request.Body body = Request.Body.create(contentType, "abc".getBytes(Util.UTF_8));
+ RequestBody body = RequestBody.create(contentType, "abc".getBytes(Util.UTF_8));
assertEquals(contentType, body.contentType());
assertEquals(3, body.contentLength());
assertEquals("616263", bodyToHex(body));
@@ -66,16 +71,69 @@
writer.close();
MediaType contentType = MediaType.parse("text/plain");
- Request.Body body = Request.Body.create(contentType, file);
+ RequestBody body = RequestBody.create(contentType, file);
assertEquals(contentType, body.contentType());
assertEquals(3, body.contentLength());
assertEquals("616263", bodyToHex(body));
assertEquals("Retransmit body", "616263", bodyToHex(body));
}
- private String bodyToHex(Request.Body body) throws IOException {
- OkBuffer buffer = new OkBuffer();
+ /** Common verbs used for apis such as GitHub, AWS, and Google Cloud. */
+ @Test public void crudVerbs() throws IOException {
+ MediaType contentType = MediaType.parse("application/json");
+ RequestBody body = RequestBody.create(contentType, "{}");
+
+ Request get = new Request.Builder().url("http://localhost/api").get().build();
+ assertEquals("GET", get.method());
+ assertNull(get.body());
+
+ Request head = new Request.Builder().url("http://localhost/api").head().build();
+ assertEquals("HEAD", head.method());
+ assertNull(head.body());
+
+ Request delete = new Request.Builder().url("http://localhost/api").delete().build();
+ assertEquals("DELETE", delete.method());
+ assertEquals(0L, delete.body().contentLength());
+
+ Request post = new Request.Builder().url("http://localhost/api").post(body).build();
+ assertEquals("POST", post.method());
+ assertEquals(body, post.body());
+
+ Request put = new Request.Builder().url("http://localhost/api").put(body).build();
+ assertEquals("PUT", put.method());
+ assertEquals(body, put.body());
+
+ Request patch = new Request.Builder().url("http://localhost/api").patch(body).build();
+ assertEquals("PATCH", patch.method());
+ assertEquals(body, patch.body());
+ }
+
+ @Test public void uninitializedURI() throws Exception {
+ Request request = new Request.Builder().url("http://localhost/api").build();
+ assertEquals(new URI("http://localhost/api"), request.uri());
+ assertEquals(new URL("http://localhost/api"), request.url());
+ }
+
+ @Test public void cacheControl() throws Exception {
+ Request request = new Request.Builder()
+ .cacheControl(new CacheControl.Builder().noCache().build())
+ .url("https://square.com")
+ .build();
+ assertEquals(Arrays.asList("no-cache"), request.headers("Cache-Control"));
+ }
+
+ @Test public void emptyCacheControlClearsAllCacheControlHeaders() throws Exception {
+ Request request = new Request.Builder()
+ .header("Cache-Control", "foo")
+ .cacheControl(new CacheControl.Builder().build())
+ .url("https://square.com")
+ .build();
+ assertEquals(Collections.<String>emptyList(), request.headers("Cache-Control"));
+ }
+
+ private String bodyToHex(RequestBody body) throws IOException {
+ Buffer buffer = new Buffer();
body.writeTo(buffer);
- return buffer.readByteString(buffer.size()).hex();
+ return buffer.readByteString().hex();
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
new file mode 100644
index 0000000..e2a5532
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2014 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.NamedRunnable;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+
+/**
+ * A limited implementation of SOCKS Protocol Version 5, intended to be similar to MockWebServer.
+ * See <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928</a>.
+ */
+public final class SocksProxy {
+ private static final int VERSION_5 = 5;
+ private static final int METHOD_NONE = 0xff;
+ private static final int METHOD_NO_AUTHENTICATION_REQUIRED = 0;
+ private static final int ADDRESS_TYPE_IPV4 = 1;
+ private static final int ADDRESS_TYPE_DOMAIN_NAME = 3;
+ private static final int COMMAND_CONNECT = 1;
+ private static final int REPLY_SUCCEEDED = 0;
+
+ private static final Logger logger = Logger.getLogger(SocksProxy.class.getName());
+
+ private final ExecutorService executor = Executors.newCachedThreadPool(
+ Util.threadFactory("SocksProxy", false));
+
+ private ServerSocket serverSocket;
+ private AtomicInteger connectionCount = new AtomicInteger();
+
+ public void play() throws IOException {
+ serverSocket = new ServerSocket(0);
+ executor.execute(new NamedRunnable("SocksProxy %s", serverSocket.getLocalPort()) {
+ @Override protected void execute() {
+ try {
+ while (true) {
+ Socket socket = serverSocket.accept();
+ connectionCount.incrementAndGet();
+ service(socket);
+ }
+ } catch (SocketException e) {
+ logger.info(name + " done accepting connections: " + e.getMessage());
+ } catch (IOException e) {
+ logger.log(Level.WARNING, name + " failed unexpectedly", e);
+ }
+ }
+ });
+ }
+
+ public Proxy proxy() {
+ return new Proxy(Proxy.Type.SOCKS, InetSocketAddress.createUnresolved(
+ "localhost", serverSocket.getLocalPort()));
+ }
+
+ public int connectionCount() {
+ return connectionCount.get();
+ }
+
+ public void shutdown() throws Exception {
+ serverSocket.close();
+ executor.shutdown();
+ if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
+ throw new IOException("Gave up waiting for executor to shut down");
+ }
+ }
+
+ private void service(final Socket from) {
+ executor.execute(new NamedRunnable("SocksProxy %s", from.getRemoteSocketAddress()) {
+ @Override protected void execute() {
+ try {
+ BufferedSource fromSource = Okio.buffer(Okio.source(from));
+ BufferedSink fromSink = Okio.buffer(Okio.sink(from));
+ hello(fromSource, fromSink);
+ acceptCommand(from.getInetAddress(), fromSource, fromSink);
+ } catch (IOException e) {
+ logger.log(Level.WARNING, name + " failed", e);
+ Util.closeQuietly(from);
+ }
+ }
+ });
+ }
+
+ private void hello(BufferedSource fromSource, BufferedSink fromSink) throws IOException {
+ int version = fromSource.readByte() & 0xff;
+ int methodCount = fromSource.readByte() & 0xff;
+ int selectedMethod = METHOD_NONE;
+
+ if (version != VERSION_5) {
+ throw new ProtocolException("unsupported version: " + version);
+ }
+
+ for (int i = 0; i < methodCount; i++) {
+ int candidateMethod = fromSource.readByte() & 0xff;
+ if (candidateMethod == METHOD_NO_AUTHENTICATION_REQUIRED) {
+ selectedMethod = candidateMethod;
+ }
+ }
+
+ switch (selectedMethod) {
+ case METHOD_NO_AUTHENTICATION_REQUIRED:
+ fromSink.writeByte(VERSION_5);
+ fromSink.writeByte(selectedMethod);
+ fromSink.emit();
+ break;
+
+ default:
+ throw new ProtocolException("unsupported method: " + selectedMethod);
+ }
+ }
+
+ private void acceptCommand(InetAddress fromAddress, BufferedSource fromSource,
+ BufferedSink fromSink) throws IOException {
+ // Read the command.
+ int version = fromSource.readByte() & 0xff;
+ if (version != VERSION_5) throw new ProtocolException("unexpected version: " + version);
+ int command = fromSource.readByte() & 0xff;
+ int reserved = fromSource.readByte() & 0xff;
+ if (reserved != 0) throw new ProtocolException("unexpected reserved: " + reserved);
+
+ int addressType = fromSource.readByte() & 0xff;
+ InetAddress toAddress;
+ switch (addressType) {
+ case ADDRESS_TYPE_IPV4:
+ toAddress = InetAddress.getByAddress(fromSource.readByteArray(4L));
+ break;
+
+ case ADDRESS_TYPE_DOMAIN_NAME:
+ int domainNameLength = fromSource.readByte() & 0xff;
+ String domainName = fromSource.readUtf8(domainNameLength);
+ toAddress = InetAddress.getByName(domainName);
+ break;
+
+ default:
+ throw new ProtocolException("unsupported address type: " + addressType);
+ }
+
+ int port = fromSource.readShort() & 0xffff;
+
+ switch (command) {
+ case COMMAND_CONNECT:
+ Socket toSocket = new Socket(toAddress, port);
+ byte[] localAddress = toSocket.getLocalAddress().getAddress();
+ if (localAddress.length != 4) {
+ throw new ProtocolException("unexpected address: " + toSocket.getLocalAddress());
+ }
+
+ // Write the reply.
+ fromSink.writeByte(VERSION_5);
+ fromSink.writeByte(REPLY_SUCCEEDED);
+ fromSink.writeByte(0);
+ fromSink.writeByte(ADDRESS_TYPE_IPV4);
+ fromSink.write(localAddress);
+ fromSink.writeShort(toSocket.getLocalPort());
+ fromSink.emit();
+
+ logger.log(Level.INFO, "SocksProxy connected " + fromAddress + " to " + toAddress);
+
+ // Copy sources to sinks in both directions.
+ BufferedSource toSource = Okio.buffer(Okio.source(toSocket));
+ BufferedSink toSink = Okio.buffer(Okio.sink(toSocket));
+ transfer(fromAddress, toAddress, fromSource, toSink);
+ transfer(fromAddress, toAddress, toSource, fromSink);
+ break;
+
+ default:
+ throw new ProtocolException("unexpected command: " + command);
+ }
+ }
+
+ private void transfer(final InetAddress fromAddress, final InetAddress toAddress,
+ final BufferedSource source, final BufferedSink sink) {
+ executor.execute(new NamedRunnable("SocksProxy %s to %s", fromAddress, toAddress) {
+ @Override protected void execute() {
+ Buffer buffer = new Buffer();
+ try {
+ while (true) {
+ long byteCount = source.read(buffer, 2048L);
+ if (byteCount == -1L) break;
+ sink.write(buffer, byteCount);
+ sink.emit();
+ }
+ } catch (SocketException e) {
+ logger.info(name + " done: " + e.getMessage());
+ } catch (IOException e) {
+ logger.log(Level.WARNING, name + " failed", e);
+ }
+
+ try {
+ source.close();
+ } catch (IOException e) {
+ logger.log(Level.WARNING, name + " failed", e);
+ }
+
+ try {
+ sink.close();
+ } catch (IOException e) {
+ logger.log(Level.WARNING, name + " failed", e);
+ }
+ }
+ });
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
new file mode 100644
index 0000000..9b10213
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2014 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.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public final class SocksProxyTest {
+ private final SocksProxy socksProxy = new SocksProxy();
+ private final MockWebServer server = new MockWebServer();
+
+ @Before public void setUp() throws Exception {
+ server.start();
+ socksProxy.play();
+ }
+
+ @After public void tearDown() throws Exception {
+ server.shutdown();
+ socksProxy.shutdown();
+ }
+
+ @Test public void proxy() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setBody("def"));
+
+ OkHttpClient client = new OkHttpClient()
+ .setProxy(socksProxy.proxy());
+
+ Request request1 = new Request.Builder().url(server.getUrl("/")).build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("abc", response1.body().string());
+
+ Request request2 = new Request.Builder().url(server.getUrl("/")).build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("def", response2.body().string());
+
+ // The HTTP calls should share a single connection.
+ assertEquals(1, socksProxy.connectionCount());
+ }
+
+ @Test public void proxySelector() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ ProxySelector proxySelector = new ProxySelector() {
+ @Override public List<Proxy> select(URI uri) {
+ return Collections.singletonList(socksProxy.proxy());
+ }
+
+ @Override public void connectFailed(URI uri, SocketAddress socketAddress, IOException e) {
+ throw new AssertionError();
+ }
+ };
+
+ OkHttpClient client = new OkHttpClient()
+ .setProxySelector(proxySelector);
+
+ Request request = new Request.Builder().url(server.getUrl("/")).build();
+ Response response = client.newCall(request).execute();
+ assertEquals("abc", response.body().string());
+
+ assertEquals(1, socksProxy.connectionCount());
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
deleted file mode 100644
index 48b680a..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * Copyright (C) 2014 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.RecordingHostnameVerifier;
-import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.mockwebserver.RecordedRequest;
-import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import java.io.File;
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.util.UUID;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public final class SyncApiTest {
- private MockWebServer server = new MockWebServer();
- private OkHttpClient client = new OkHttpClient();
-
- private static final SSLContext sslContext = SslContextBuilder.localhost();
- private HttpResponseCache cache;
-
- @Before public void setUp() throws Exception {
- String tmp = System.getProperty("java.io.tmpdir");
- File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
- cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
- }
-
- @After public void tearDown() throws Exception {
- server.shutdown();
- cache.delete();
- }
-
- @Test public void get() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("abc")
- .addHeader("Content-Type: text/plain"));
- server.play();
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .header("User-Agent", "SyncApiTest")
- .build();
-
- onSuccess(request)
- .assertCode(200)
- .assertContainsHeaders("Content-Type: text/plain")
- .assertBody("abc");
-
- assertTrue(server.takeRequest().getHeaders().contains("User-Agent: SyncApiTest"));
- }
-
- @Test public void connectionPooling() throws Exception {
- server.enqueue(new MockResponse().setBody("abc"));
- server.enqueue(new MockResponse().setBody("def"));
- server.enqueue(new MockResponse().setBody("ghi"));
- server.play();
-
- onSuccess(new Request.Builder().url(server.getUrl("/a")).build())
- .assertBody("abc");
-
- onSuccess(new Request.Builder().url(server.getUrl("/b")).build())
- .assertBody("def");
-
- onSuccess(new Request.Builder().url(server.getUrl("/c")).build())
- .assertBody("ghi");
-
- assertEquals(0, server.takeRequest().getSequenceNumber());
- assertEquals(1, server.takeRequest().getSequenceNumber());
- assertEquals(2, server.takeRequest().getSequenceNumber());
- }
-
- @Test public void tls() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
- server.enqueue(new MockResponse()
- .setBody("abc")
- .addHeader("Content-Type: text/plain"));
- server.play();
-
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
- onSuccess(new Request.Builder().url(server.getUrl("/")).build())
- .assertHandshake();
- }
-
- @Test public void recoverFromTlsHandshakeFailure() throws Exception {
- SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
- server.useHttps(socketFactory, false);
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
- server.enqueue(new MockResponse().setBody("abc"));
- server.play();
-
- final boolean disableTlsFallbackScsv = true;
- SSLSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
- onSuccess(new Request.Builder().url(server.getUrl("/")).build())
- .assertBody("abc");
- }
-
- @Test public void post() throws Exception {
- server.enqueue(new MockResponse().setBody("abc"));
- server.play();
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .post(Request.Body.create(MediaType.parse("text/plain"), "def"))
- .build();
-
- onSuccess(request)
- .assertCode(200)
- .assertBody("abc");
-
- RecordedRequest recordedRequest = server.takeRequest();
- assertEquals("def", recordedRequest.getUtf8Body());
- assertEquals("3", recordedRequest.getHeader("Content-Length"));
- assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
- }
-
- @Test public void conditionalCacheHit() throws Exception {
- server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
- server.enqueue(new MockResponse()
- .clearHeaders()
- .setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
-
- client.setOkResponseCache(cache);
-
- onSuccess(new Request.Builder().url(server.getUrl("/")).build())
- .assertCode(200).assertBody("A");
- assertNull(server.takeRequest().getHeader("If-None-Match"));
-
- onSuccess(new Request.Builder().url(server.getUrl("/")).build())
- .assertCode(200).assertBody("A");
- assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
- }
-
- @Test public void conditionalCacheMiss() throws Exception {
- server.enqueue(new MockResponse().setBody("A").addHeader("ETag: v1"));
- server.enqueue(new MockResponse().setBody("B"));
- server.play();
-
- client.setOkResponseCache(cache);
-
- onSuccess(new Request.Builder().url(server.getUrl("/")).build())
- .assertCode(200).assertBody("A");
- assertNull(server.takeRequest().getHeader("If-None-Match"));
-
- onSuccess(new Request.Builder().url(server.getUrl("/")).build())
- .assertCode(200).assertBody("B");
- assertEquals("v1", server.takeRequest().getHeader("If-None-Match"));
- }
-
- @Test public void redirect() throws Exception {
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /b")
- .addHeader("Test", "Redirect from /a to /b")
- .setBody("/a has moved!"));
- server.enqueue(new MockResponse()
- .setResponseCode(302)
- .addHeader("Location: /c")
- .addHeader("Test", "Redirect from /b to /c")
- .setBody("/b has moved!"));
- server.enqueue(new MockResponse().setBody("C"));
- server.play();
-
- onSuccess(new Request.Builder().url(server.getUrl("/a")).build())
- .assertCode(200)
- .assertBody("C")
- .redirectedBy()
- .assertCode(302)
- .assertContainsHeaders("Test: Redirect from /b to /c")
- .redirectedBy()
- .assertCode(301)
- .assertContainsHeaders("Test: Redirect from /a to /b");
-
- assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection.
- assertEquals(1, server.takeRequest().getSequenceNumber()); // Connection reused.
- assertEquals(2, server.takeRequest().getSequenceNumber()); // Connection reused again!
- }
-
- @Test public void redirectWithRedirectsDisabled() throws Exception {
- client.setFollowProtocolRedirects(false);
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /b")
- .addHeader("Test", "Redirect from /a to /b")
- .setBody("/a has moved!"));
- server.play();
-
- onSuccess(new Request.Builder().url(server.getUrl("/a")).build())
- .assertCode(301)
- .assertBody("/a has moved!")
- .assertContainsHeaders("Location: /b");
- }
-
- @Test public void follow20Redirects() throws Exception {
- for (int i = 0; i < 20; i++) {
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /" + (i + 1))
- .setBody("Redirecting to /" + (i + 1)));
- }
- server.enqueue(new MockResponse().setBody("Success!"));
- server.play();
-
- onSuccess(new Request.Builder().url(server.getUrl("/0")).build())
- .assertCode(200)
- .assertBody("Success!");
- }
-
- @Test public void doesNotFollow21Redirects() throws Exception {
- for (int i = 0; i < 21; i++) {
- server.enqueue(new MockResponse()
- .setResponseCode(301)
- .addHeader("Location: /" + (i + 1))
- .setBody("Redirecting to /" + (i + 1)));
- }
- server.play();
-
- try {
- client.execute(new Request.Builder().url(server.getUrl("/0")).build());
- fail();
- } catch (IOException e) {
- assertEquals("Too many redirects: 21", e.getMessage());
- }
- }
-
- @Test public void postBodyRetransmittedOnRedirect() throws Exception {
- server.enqueue(new MockResponse()
- .setResponseCode(302)
- .addHeader("Location: /b")
- .setBody("Moved to /b !"));
- server.enqueue(new MockResponse()
- .setBody("This is b."));
- server.play();
-
- Request request = new Request.Builder()
- .url(server.getUrl("/"))
- .post(Request.Body.create(MediaType.parse("text/plain"), "body!"))
- .build();
-
- onSuccess(request)
- .assertCode(200)
- .assertBody("This is b.");
-
- RecordedRequest request1 = server.takeRequest();
- assertEquals("body!", request1.getUtf8Body());
- assertEquals("5", request1.getHeader("Content-Length"));
- assertEquals("text/plain; charset=utf-8", request1.getHeader("Content-Type"));
- assertEquals(0, request1.getSequenceNumber());
-
- RecordedRequest request2 = server.takeRequest();
- assertEquals("body!", request2.getUtf8Body());
- assertEquals("5", request2.getHeader("Content-Length"));
- assertEquals("text/plain; charset=utf-8", request2.getHeader("Content-Type"));
- assertEquals(1, request2.getSequenceNumber());
- }
-
- private RecordedResponse onSuccess(Request request) throws IOException {
- Response response = client.execute(request);
- return new RecordedResponse(request, response, response.body().string(), null);
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/TestLogHandler.java b/okhttp-tests/src/test/java/com/squareup/okhttp/TestLogHandler.java
new file mode 100644
index 0000000..24cb377
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/TestLogHandler.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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.util.ArrayList;
+import java.util.List;
+import java.util.logging.Handler;
+import java.util.logging.LogRecord;
+
+/**
+ * A log handler that records which log messages were published so that a calling test can make
+ * assertions about them.
+ */
+public final class TestLogHandler extends Handler {
+ private final List<String> logs = new ArrayList<>();
+
+ @Override public synchronized void publish(LogRecord logRecord) {
+ logs.add(logRecord.getLevel() + ": " + logRecord.getMessage());
+ notifyAll();
+ }
+
+ @Override public void flush() {
+ }
+
+ @Override public void close() throws SecurityException {
+ }
+
+ public synchronized String take() throws InterruptedException {
+ while (logs.isEmpty()) {
+ wait();
+ }
+ return logs.remove(0);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java b/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java
new file mode 100644
index 0000000..10f0d4d
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/TestUtil.java
@@ -0,0 +1,18 @@
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.spdy.Header;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class TestUtil {
+ private TestUtil() {
+ }
+
+ public static List<Header> headerEntries(String... elements) {
+ List<Header> result = new ArrayList<>(elements.length / 2);
+ for (int i = 0; i < elements.length; i += 2) {
+ result.add(new Header(elements[i], elements[i + 1]));
+ }
+ return result;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/BitArrayTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/BitArrayTest.java
deleted file mode 100644
index 7f80c3b..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/BitArrayTest.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright 2014 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 org.junit.Test;
-
-import static java.util.Arrays.asList;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public class BitArrayTest {
-
- /** Lazy grow into a variable capacity bit set. */
- @Test public void hpackUseCase() {
- BitArray b = new BitArray.FixedCapacity();
- for (int i = 0; i < 64; i++) {
- b.set(i);
- }
- assertTrue(b.get(0));
- assertTrue(b.get(1));
- assertTrue(b.get(63));
- try {
- b.get(64);
- fail();
- } catch (IllegalArgumentException expected) {
- }
- b = ((BitArray.FixedCapacity) b).toVariableCapacity();
- assertTrue(b.get(0));
- assertTrue(b.get(1));
- assertTrue(b.get(63));
- assertFalse(b.get(64));
- b.set(64);
- assertTrue(b.get(64));
- }
-
- @Test public void setExpandsData_FixedCapacity() {
- BitArray.FixedCapacity b = new BitArray.FixedCapacity();
- b.set(63);
- assertEquals(b.data, BigInteger.ZERO.setBit(63).longValue());
- }
-
- @Test public void toggleBit_FixedCapacity() {
- BitArray.FixedCapacity b = new BitArray.FixedCapacity();
- b.set(63);
- b.toggle(63);
- assertEquals(b.data, 0l);
- b.toggle(1);
- assertEquals(b.data, 2l);
- }
-
- @Test public void shiftLeft_FixedCapacity() {
- BitArray.FixedCapacity b = new BitArray.FixedCapacity();
- b.set(0);
- b.shiftLeft(1);
- assertEquals(b.data, 2l);
- }
-
- @Test public void multipleShifts_FixedCapacity() {
- BitArray.FixedCapacity b = new BitArray.FixedCapacity();
- b.set(10);
- b.shiftLeft(2);
- b.shiftLeft(2);
- assertEquals(b.data, BigInteger.ZERO.setBit(10).shiftLeft(2).shiftLeft(2).longValue());
- }
-
- @Test public void clearBits_FixedCapacity() {
- BitArray.FixedCapacity b = new BitArray.FixedCapacity();
- b.set(1);
- b.set(3);
- b.set(5);
- b.clear();
- assertEquals(b.data, 0l);
- }
-
- @Test public void setExpandsData_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(64);
- assertEquals(asList(64), b.toIntegerList());
- }
-
- @Test public void toggleBit_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(100);
- b.toggle(100);
- assertTrue(b.toIntegerList().isEmpty());
- b.toggle(1);
- assertEquals(asList(1), b.toIntegerList());
- }
-
- @Test public void shiftLeftExpandsData_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(0);
- b.shiftLeft(64);
- assertEquals(asList(64), b.toIntegerList());
- }
-
- @Test public void shiftLeftFromZero_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(0);
- b.shiftLeft(1);
- assertEquals(asList(1), b.toIntegerList());
- }
-
- @Test public void shiftLeftAcrossOffset_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(63);
- assertEquals(1, b.data.length);
- b.shiftLeft(1);
- assertEquals(asList(64), b.toIntegerList());
- assertEquals(2, b.data.length);
- }
-
- @Test public void multipleShiftsLeftAcrossOffset_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(1000);
- b.shiftLeft(67);
- assertEquals(asList(1067), b.toIntegerList());
- b.shiftLeft(69);
- assertEquals(asList(1136), b.toIntegerList());
- }
-
- @Test public void clearBits_VariableCapacity() {
- BitArray.VariableCapacity b = new BitArray.VariableCapacity();
- b.set(10);
- b.set(100);
- b.set(1000);
- b.clear();
- assertTrue(b.toIntegerList().isEmpty());
- }
-
- @Test public void bigIntegerSanityCheck_VariableCapacity() {
- BitArray a = new BitArray.VariableCapacity();
- BigInteger b = BigInteger.ZERO;
-
- a.set(64);
- b = b.setBit(64);
- assertEquals(bigIntegerToString(b), a.toString());
-
- a.set(1000000);
- b = b.setBit(1000000);
- assertEquals(bigIntegerToString(b), a.toString());
-
- a.shiftLeft(100);
- b = b.shiftLeft(100);
- assertEquals(bigIntegerToString(b), a.toString());
-
- a.set(0xF00D);
- b = b.setBit(0xF00D);
- a.set(0xBEEF);
- b = b.setBit(0xBEEF);
- a.set(0xDEAD);
- b = b.setBit(0xDEAD);
- assertEquals(bigIntegerToString(b), a.toString());
-
- a.shiftLeft(0xB0B);
- b = b.shiftLeft(0xB0B);
- assertEquals(bigIntegerToString(b), a.toString());
-
- a.toggle(64280);
- b = b.clearBit(64280);
- assertEquals(bigIntegerToString(b), a.toString());
- }
-
- private static String bigIntegerToString(BigInteger b) {
- StringBuilder builder = new StringBuilder("{");
- for (int i = 0, count = b.bitLength(); i < count; i++) {
- if (b.testBit(i)) {
- builder.append(i).append(',');
- }
- }
- builder.setCharAt(builder.length() - 1, '}');
- return builder.toString();
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
new file mode 100644
index 0000000..7326a0d
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DiskLruCacheTest.java
@@ -0,0 +1,1252 @@
+/*
+ * 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;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.Timeout;
+
+import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE;
+import static com.squareup.okhttp.internal.DiskLruCache.JOURNAL_FILE_BACKUP;
+import static com.squareup.okhttp.internal.DiskLruCache.MAGIC;
+import static com.squareup.okhttp.internal.DiskLruCache.VERSION_1;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class DiskLruCacheTest {
+ @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
+ @Rule public final Timeout timeout = new Timeout(30 * 1000);
+
+ private final int appVersion = 100;
+ private File cacheDir;
+ private File journalFile;
+ private File journalBkpFile;
+ private final TestExecutor executor = new TestExecutor();
+
+ private DiskLruCache cache;
+ private final Deque<DiskLruCache> toClose = new ArrayDeque<>();
+
+ private void createNewCache() throws IOException {
+ createNewCacheWithSize(Integer.MAX_VALUE);
+ }
+
+ private void createNewCacheWithSize(int maxSize) throws IOException {
+ cache = new DiskLruCache(cacheDir, appVersion, 2, maxSize, executor);
+ synchronized (cache) {
+ cache.initialize();
+ }
+ toClose.add(cache);
+ }
+
+ @Before public void setUp() throws Exception {
+ cacheDir = tempDir.getRoot();
+ journalFile = new File(cacheDir, JOURNAL_FILE);
+ journalBkpFile = new File(cacheDir, JOURNAL_FILE_BACKUP);
+ createNewCache();
+ }
+
+ @After public void tearDown() throws Exception {
+ while (!toClose.isEmpty()) {
+ toClose.pop().close();
+ }
+ }
+
+ @Test public void emptyCache() throws Exception {
+ cache.close();
+ assertJournalEquals();
+ }
+
+ @Test public void validateKey() throws Exception {
+ String key = null;
+ try {
+ key = "has_space ";
+ cache.edit(key);
+ fail("Exepcting an IllegalArgumentException as the key was invalid.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
+ }
+ try {
+ key = "has_CR\r";
+ cache.edit(key);
+ fail("Exepcting an IllegalArgumentException as the key was invalid.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
+ }
+ try {
+ key = "has_LF\n";
+ cache.edit(key);
+ fail("Exepcting an IllegalArgumentException as the key was invalid.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
+ }
+ try {
+ key = "has_invalid/";
+ cache.edit(key);
+ fail("Exepcting an IllegalArgumentException as the key was invalid.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
+ }
+ try {
+ key = "has_invalid\u2603";
+ cache.edit(key);
+ fail("Exepcting an IllegalArgumentException as the key was invalid.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
+ }
+ try {
+ key = "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long_"
+ + "this_is_way_too_long_this_is_way_too_long_this_is_way_too_long";
+ cache.edit(key);
+ fail("Exepcting an IllegalArgumentException as the key was too long.");
+ } catch (IllegalArgumentException iae) {
+ assertEquals("keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"", iae.getMessage());
+ }
+
+ // Test valid cases.
+
+ // Exactly 120.
+ key = "0123456789012345678901234567890123456789012345678901234567890123456789"
+ + "01234567890123456789012345678901234567890123456789";
+ cache.edit(key).abort();
+ // Contains all valid characters.
+ key = "abcdefghijklmnopqrstuvwxyz_0123456789";
+ cache.edit(key).abort();
+ // Contains dash.
+ key = "-20384573948576";
+ cache.edit(key).abort();
+ }
+
+ @Test public void writeAndReadEntry() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 0, "ABC");
+ setString(creator, 1, "DE");
+ assertNull(creator.newSource(0));
+ assertNull(creator.newSource(1));
+ creator.commit();
+
+ DiskLruCache.Snapshot snapshot = cache.get("k1");
+ assertSnapshotValue(snapshot, 0, "ABC");
+ assertSnapshotValue(snapshot, 1, "DE");
+ }
+
+ @Test public void readAndWriteEntryAcrossCacheOpenAndClose() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 0, "A");
+ setString(creator, 1, "B");
+ creator.commit();
+ cache.close();
+
+ createNewCache();
+ DiskLruCache.Snapshot snapshot = cache.get("k1");
+ assertSnapshotValue(snapshot, 0, "A");
+ assertSnapshotValue(snapshot, 1, "B");
+ snapshot.close();
+ }
+
+ @Test public void readAndWriteEntryWithoutProperClose() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 0, "A");
+ setString(creator, 1, "B");
+ creator.commit();
+
+ // Simulate a dirty close of 'cache' by opening the cache directory again.
+ createNewCache();
+ DiskLruCache.Snapshot snapshot = cache.get("k1");
+ assertSnapshotValue(snapshot, 0, "A");
+ assertSnapshotValue(snapshot, 1, "B");
+ snapshot.close();
+ }
+
+ @Test public void journalWithEditAndPublish() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
+ setString(creator, 0, "AB");
+ setString(creator, 1, "C");
+ creator.commit();
+ cache.close();
+ assertJournalEquals("DIRTY k1", "CLEAN k1 2 1");
+ }
+
+ @Test public void revertedNewFileIsRemoveInJournal() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ assertJournalEquals("DIRTY k1"); // DIRTY must always be flushed.
+ setString(creator, 0, "AB");
+ setString(creator, 1, "C");
+ creator.abort();
+ cache.close();
+ assertJournalEquals("DIRTY k1", "REMOVE k1");
+ }
+
+ @Test public void unterminatedEditIsRevertedOnClose() throws Exception {
+ cache.edit("k1");
+ cache.close();
+ assertJournalEquals("DIRTY k1", "REMOVE k1");
+ }
+
+ @Test public void journalDoesNotIncludeReadOfYetUnpublishedValue() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ assertNull(cache.get("k1"));
+ setString(creator, 0, "A");
+ setString(creator, 1, "BC");
+ creator.commit();
+ cache.close();
+ assertJournalEquals("DIRTY k1", "CLEAN k1 1 2");
+ }
+
+ @Test public void journalWithEditAndPublishAndRead() throws Exception {
+ DiskLruCache.Editor k1Creator = cache.edit("k1");
+ setString(k1Creator, 0, "AB");
+ setString(k1Creator, 1, "C");
+ k1Creator.commit();
+ DiskLruCache.Editor k2Creator = cache.edit("k2");
+ setString(k2Creator, 0, "DEF");
+ setString(k2Creator, 1, "G");
+ k2Creator.commit();
+ DiskLruCache.Snapshot k1Snapshot = cache.get("k1");
+ k1Snapshot.close();
+ cache.close();
+ assertJournalEquals("DIRTY k1", "CLEAN k1 2 1", "DIRTY k2", "CLEAN k2 3 1", "READ k1");
+ }
+
+ @Test public void cannotOperateOnEditAfterPublish() throws Exception {
+ DiskLruCache.Editor editor = cache.edit("k1");
+ setString(editor, 0, "A");
+ setString(editor, 1, "B");
+ editor.commit();
+ assertInoperable(editor);
+ }
+
+ @Test public void cannotOperateOnEditAfterRevert() throws Exception {
+ DiskLruCache.Editor editor = cache.edit("k1");
+ setString(editor, 0, "A");
+ setString(editor, 1, "B");
+ editor.abort();
+ assertInoperable(editor);
+ }
+
+ @Test public void explicitRemoveAppliedToDiskImmediately() throws Exception {
+ DiskLruCache.Editor editor = cache.edit("k1");
+ setString(editor, 0, "ABC");
+ setString(editor, 1, "B");
+ editor.commit();
+ File k1 = getCleanFile("k1", 0);
+ assertEquals("ABC", readFile(k1));
+ cache.remove("k1");
+ assertFalse(k1.exists());
+ }
+
+ @Test public void removePreventsActiveEditFromStoringAValue() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Editor a = cache.edit("a");
+ setString(a, 0, "a1");
+ assertTrue(cache.remove("a"));
+ setString(a, 1, "a2");
+ a.commit();
+ assertAbsent("a");
+ }
+
+ /**
+ * Each read sees a snapshot of the file at the time read was called.
+ * This means that two reads of the same key can see different data.
+ */
+ @Test public void readAndWriteOverlapsMaintainConsistency() throws Exception {
+ DiskLruCache.Editor v1Creator = cache.edit("k1");
+ setString(v1Creator, 0, "AAaa");
+ setString(v1Creator, 1, "BBbb");
+ v1Creator.commit();
+
+ DiskLruCache.Snapshot snapshot1 = cache.get("k1");
+ BufferedSource inV1 = Okio.buffer(snapshot1.getSource(0));
+ assertEquals('A', inV1.readByte());
+ assertEquals('A', inV1.readByte());
+
+ DiskLruCache.Editor v1Updater = cache.edit("k1");
+ setString(v1Updater, 0, "CCcc");
+ setString(v1Updater, 1, "DDdd");
+ v1Updater.commit();
+
+ DiskLruCache.Snapshot snapshot2 = cache.get("k1");
+ assertSnapshotValue(snapshot2, 0, "CCcc");
+ assertSnapshotValue(snapshot2, 1, "DDdd");
+ snapshot2.close();
+
+ assertEquals('a', inV1.readByte());
+ assertEquals('a', inV1.readByte());
+ assertSnapshotValue(snapshot1, 1, "BBbb");
+ snapshot1.close();
+ }
+
+ @Test public void openWithDirtyKeyDeletesAllFilesForThatKey() throws Exception {
+ cache.close();
+ File cleanFile0 = getCleanFile("k1", 0);
+ File cleanFile1 = getCleanFile("k1", 1);
+ File dirtyFile0 = getDirtyFile("k1", 0);
+ File dirtyFile1 = getDirtyFile("k1", 1);
+ writeFile(cleanFile0, "A");
+ writeFile(cleanFile1, "B");
+ writeFile(dirtyFile0, "C");
+ writeFile(dirtyFile1, "D");
+ createJournal("CLEAN k1 1 1", "DIRTY k1");
+ createNewCache();
+ assertFalse(cleanFile0.exists());
+ assertFalse(cleanFile1.exists());
+ assertFalse(dirtyFile0.exists());
+ assertFalse(dirtyFile1.exists());
+ assertNull(cache.get("k1"));
+ }
+
+ @Test public void openWithInvalidVersionClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournalWithHeader(MAGIC, "0", "100", "2", "");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ }
+
+ @Test public void openWithInvalidAppVersionClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournalWithHeader(MAGIC, "1", "101", "2", "");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ }
+
+ @Test public void openWithInvalidValueCountClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournalWithHeader(MAGIC, "1", "100", "1", "");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ }
+
+ @Test public void openWithInvalidBlankLineClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournalWithHeader(MAGIC, "1", "100", "2", "x");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ }
+
+ @Test public void openWithInvalidJournalLineClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournal("CLEAN k1 1 1", "BOGUS");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ assertNull(cache.get("k1"));
+ }
+
+ @Test public void openWithInvalidFileSizeClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournal("CLEAN k1 0000x001 1");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ assertNull(cache.get("k1"));
+ }
+
+ @Test public void openWithTruncatedLineDiscardsThatLine() throws Exception {
+ cache.close();
+ writeFile(getCleanFile("k1", 0), "A");
+ writeFile(getCleanFile("k1", 1), "B");
+ Writer writer = new FileWriter(journalFile);
+ writer.write(MAGIC + "\n" + VERSION_1 + "\n100\n2\n\nCLEAN k1 1 1"); // no trailing newline
+ writer.close();
+ createNewCache();
+ assertNull(cache.get("k1"));
+
+ // The journal is not corrupt when editing after a truncated line.
+ set("k1", "C", "D");
+
+ cache.close();
+ createNewCache();
+ assertValue("k1", "C", "D");
+ }
+
+ @Test public void openWithTooManyFileSizesClearsDirectory() throws Exception {
+ cache.close();
+ generateSomeGarbageFiles();
+ createJournal("CLEAN k1 1 1 1");
+ createNewCache();
+ assertGarbageFilesAllDeleted();
+ assertNull(cache.get("k1"));
+ }
+
+ @Test public void keyWithSpaceNotPermitted() throws Exception {
+ try {
+ cache.edit("my key");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void keyWithNewlineNotPermitted() throws Exception {
+ try {
+ cache.edit("my\nkey");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void keyWithCarriageReturnNotPermitted() throws Exception {
+ try {
+ cache.edit("my\rkey");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void nullKeyThrows() throws Exception {
+ try {
+ cache.edit(null);
+ fail();
+ } catch (NullPointerException expected) {
+ }
+ }
+
+ @Test public void createNewEntryWithTooFewValuesFails() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 1, "A");
+ try {
+ creator.commit();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ assertFalse(getCleanFile("k1", 0).exists());
+ assertFalse(getCleanFile("k1", 1).exists());
+ assertFalse(getDirtyFile("k1", 0).exists());
+ assertFalse(getDirtyFile("k1", 1).exists());
+ assertNull(cache.get("k1"));
+
+ DiskLruCache.Editor creator2 = cache.edit("k1");
+ setString(creator2, 0, "B");
+ setString(creator2, 1, "C");
+ creator2.commit();
+ }
+
+ @Test public void revertWithTooFewValues() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 1, "A");
+ creator.abort();
+ assertFalse(getCleanFile("k1", 0).exists());
+ assertFalse(getCleanFile("k1", 1).exists());
+ assertFalse(getDirtyFile("k1", 0).exists());
+ assertFalse(getDirtyFile("k1", 1).exists());
+ assertNull(cache.get("k1"));
+ }
+
+ @Test public void updateExistingEntryWithTooFewValuesReusesPreviousValues() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 0, "A");
+ setString(creator, 1, "B");
+ creator.commit();
+
+ DiskLruCache.Editor updater = cache.edit("k1");
+ setString(updater, 0, "C");
+ updater.commit();
+
+ DiskLruCache.Snapshot snapshot = cache.get("k1");
+ assertSnapshotValue(snapshot, 0, "C");
+ assertSnapshotValue(snapshot, 1, "B");
+ snapshot.close();
+ }
+
+ @Test public void growMaxSize() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+ set("a", "a", "aaa"); // size 4
+ set("b", "bb", "bbbb"); // size 6
+ cache.setMaxSize(20);
+ set("c", "c", "c"); // size 12
+ assertEquals(12, cache.size());
+ }
+
+ @Test public void shrinkMaxSizeEvicts() throws Exception {
+ cache.close();
+ createNewCacheWithSize(20);
+ set("a", "a", "aaa"); // size 4
+ set("b", "bb", "bbbb"); // size 6
+ set("c", "c", "c"); // size 12
+ cache.setMaxSize(10);
+ assertEquals(1, executor.jobs.size());
+ }
+
+ @Test public void evictOnInsert() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+
+ set("a", "a", "aaa"); // size 4
+ set("b", "bb", "bbbb"); // size 6
+ assertEquals(10, cache.size());
+
+ // Cause the size to grow to 12 should evict 'A'.
+ set("c", "c", "c");
+ cache.flush();
+ assertEquals(8, cache.size());
+ assertAbsent("a");
+ assertValue("b", "bb", "bbbb");
+ assertValue("c", "c", "c");
+
+ // Causing the size to grow to 10 should evict nothing.
+ set("d", "d", "d");
+ cache.flush();
+ assertEquals(10, cache.size());
+ assertAbsent("a");
+ assertValue("b", "bb", "bbbb");
+ assertValue("c", "c", "c");
+ assertValue("d", "d", "d");
+
+ // Causing the size to grow to 18 should evict 'B' and 'C'.
+ set("e", "eeee", "eeee");
+ cache.flush();
+ assertEquals(10, cache.size());
+ assertAbsent("a");
+ assertAbsent("b");
+ assertAbsent("c");
+ assertValue("d", "d", "d");
+ assertValue("e", "eeee", "eeee");
+ }
+
+ @Test public void evictOnUpdate() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+
+ set("a", "a", "aa"); // size 3
+ set("b", "b", "bb"); // size 3
+ set("c", "c", "cc"); // size 3
+ assertEquals(9, cache.size());
+
+ // Causing the size to grow to 11 should evict 'A'.
+ set("b", "b", "bbbb");
+ cache.flush();
+ assertEquals(8, cache.size());
+ assertAbsent("a");
+ assertValue("b", "b", "bbbb");
+ assertValue("c", "c", "cc");
+ }
+
+ @Test public void evictionHonorsLruFromCurrentSession() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+ set("a", "a", "a");
+ set("b", "b", "b");
+ set("c", "c", "c");
+ set("d", "d", "d");
+ set("e", "e", "e");
+ cache.get("b").close(); // 'B' is now least recently used.
+
+ // Causing the size to grow to 12 should evict 'A'.
+ set("f", "f", "f");
+ // Causing the size to grow to 12 should evict 'C'.
+ set("g", "g", "g");
+ cache.flush();
+ assertEquals(10, cache.size());
+ assertAbsent("a");
+ assertValue("b", "b", "b");
+ assertAbsent("c");
+ assertValue("d", "d", "d");
+ assertValue("e", "e", "e");
+ assertValue("f", "f", "f");
+ }
+
+ @Test public void evictionHonorsLruFromPreviousSession() throws Exception {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ set("c", "c", "c");
+ set("d", "d", "d");
+ set("e", "e", "e");
+ set("f", "f", "f");
+ cache.get("b").close(); // 'B' is now least recently used.
+ assertEquals(12, cache.size());
+ cache.close();
+ createNewCacheWithSize(10);
+
+ set("g", "g", "g");
+ cache.flush();
+ assertEquals(10, cache.size());
+ assertAbsent("a");
+ assertValue("b", "b", "b");
+ assertAbsent("c");
+ assertValue("d", "d", "d");
+ assertValue("e", "e", "e");
+ assertValue("f", "f", "f");
+ assertValue("g", "g", "g");
+ }
+
+ @Test public void cacheSingleEntryOfSizeGreaterThanMaxSize() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+ set("a", "aaaaa", "aaaaaa"); // size=11
+ cache.flush();
+ assertAbsent("a");
+ }
+
+ @Test public void cacheSingleValueOfSizeGreaterThanMaxSize() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+ set("a", "aaaaaaaaaaa", "a"); // size=12
+ cache.flush();
+ assertAbsent("a");
+ }
+
+ @Test public void constructorDoesNotAllowZeroCacheSize() throws Exception {
+ try {
+ DiskLruCache.create(cacheDir, appVersion, 2, 0);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void constructorDoesNotAllowZeroValuesPerEntry() throws Exception {
+ try {
+ DiskLruCache.create(cacheDir, appVersion, 0, 10);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void removeAbsentElement() throws Exception {
+ cache.remove("a");
+ }
+
+ @Test public void readingTheSameStreamMultipleTimes() throws Exception {
+ set("a", "a", "b");
+ DiskLruCache.Snapshot snapshot = cache.get("a");
+ assertSame(snapshot.getSource(0), snapshot.getSource(0));
+ snapshot.close();
+ }
+
+ @Test public void rebuildJournalOnRepeatedReads() throws Exception {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ while (executor.jobs.isEmpty()) {
+ assertValue("a", "a", "a");
+ assertValue("b", "b", "b");
+ }
+ }
+
+ @Test public void rebuildJournalOnRepeatedEdits() throws Exception {
+ while (executor.jobs.isEmpty()) {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ }
+ executor.jobs.removeFirst().run();
+
+ // Sanity check that a rebuilt journal behaves normally.
+ assertValue("a", "a", "a");
+ assertValue("b", "b", "b");
+ }
+
+ /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
+ @Test public void rebuildJournalOnRepeatedReadsWithOpenAndClose() throws Exception {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ while (executor.jobs.isEmpty()) {
+ assertValue("a", "a", "a");
+ assertValue("b", "b", "b");
+ cache.close();
+ createNewCache();
+ }
+ }
+
+ /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/28">Issue #28</a> */
+ @Test public void rebuildJournalOnRepeatedEditsWithOpenAndClose() throws Exception {
+ while (executor.jobs.isEmpty()) {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ cache.close();
+ createNewCache();
+ }
+ }
+
+ @Test public void restoreBackupFile() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 0, "ABC");
+ setString(creator, 1, "DE");
+ creator.commit();
+ cache.close();
+
+ assertTrue(journalFile.renameTo(journalBkpFile));
+ assertFalse(journalFile.exists());
+
+ createNewCache();
+
+ DiskLruCache.Snapshot snapshot = cache.get("k1");
+ assertSnapshotValue(snapshot, 0, "ABC");
+ assertSnapshotValue(snapshot, 1, "DE");
+
+ assertFalse(journalBkpFile.exists());
+ assertTrue(journalFile.exists());
+ }
+
+ @Test public void journalFileIsPreferredOverBackupFile() throws Exception {
+ DiskLruCache.Editor creator = cache.edit("k1");
+ setString(creator, 0, "ABC");
+ setString(creator, 1, "DE");
+ creator.commit();
+ cache.flush();
+
+ copyFile(journalFile, journalBkpFile);
+
+ creator = cache.edit("k2");
+ setString(creator, 0, "F");
+ setString(creator, 1, "GH");
+ creator.commit();
+ cache.close();
+
+ assertTrue(journalFile.exists());
+ assertTrue(journalBkpFile.exists());
+
+ createNewCache();
+
+ DiskLruCache.Snapshot snapshotA = cache.get("k1");
+ assertSnapshotValue(snapshotA, 0, "ABC");
+ assertSnapshotValue(snapshotA, 1, "DE");
+
+ DiskLruCache.Snapshot snapshotB = cache.get("k2");
+ assertSnapshotValue(snapshotB, 0, "F");
+ assertSnapshotValue(snapshotB, 1, "GH");
+
+ assertFalse(journalBkpFile.exists());
+ assertTrue(journalFile.exists());
+ }
+
+ @Test public void openCreatesDirectoryIfNecessary() throws Exception {
+ cache.close();
+ File dir = tempDir.newFolder("testOpenCreatesDirectoryIfNecessary");
+ cache = DiskLruCache.create(dir, appVersion, 2, Integer.MAX_VALUE);
+ set("a", "a", "a");
+ assertTrue(new File(dir, "a.0").exists());
+ assertTrue(new File(dir, "a.1").exists());
+ assertTrue(new File(dir, "journal").exists());
+ }
+
+ @Test public void fileDeletedExternally() throws Exception {
+ set("a", "a", "a");
+ getCleanFile("a", 1).delete();
+ assertNull(cache.get("a"));
+ }
+
+ @Test public void editSameVersion() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Snapshot snapshot = cache.get("a");
+ DiskLruCache.Editor editor = snapshot.edit();
+ setString(editor, 1, "a2");
+ editor.commit();
+ assertValue("a", "a", "a2");
+ }
+
+ @Test public void editSnapshotAfterChangeAborted() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Snapshot snapshot = cache.get("a");
+ DiskLruCache.Editor toAbort = snapshot.edit();
+ setString(toAbort, 0, "b");
+ toAbort.abort();
+ DiskLruCache.Editor editor = snapshot.edit();
+ setString(editor, 1, "a2");
+ editor.commit();
+ assertValue("a", "a", "a2");
+ }
+
+ @Test public void editSnapshotAfterChangeCommitted() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Snapshot snapshot = cache.get("a");
+ DiskLruCache.Editor toAbort = snapshot.edit();
+ setString(toAbort, 0, "b");
+ toAbort.commit();
+ assertNull(snapshot.edit());
+ }
+
+ @Test public void editSinceEvicted() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+ set("a", "aa", "aaa"); // size 5
+ DiskLruCache.Snapshot snapshot = cache.get("a");
+ set("b", "bb", "bbb"); // size 5
+ set("c", "cc", "ccc"); // size 5; will evict 'A'
+ cache.flush();
+ assertNull(snapshot.edit());
+ }
+
+ @Test public void editSinceEvictedAndRecreated() throws Exception {
+ cache.close();
+ createNewCacheWithSize(10);
+ set("a", "aa", "aaa"); // size 5
+ DiskLruCache.Snapshot snapshot = cache.get("a");
+ set("b", "bb", "bbb"); // size 5
+ set("c", "cc", "ccc"); // size 5; will evict 'A'
+ set("a", "a", "aaaa"); // size 5; will evict 'B'
+ cache.flush();
+ assertNull(snapshot.edit());
+ }
+
+ /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
+ @Test public void aggressiveClearingHandlesWrite() throws Exception {
+ tempDir.delete();
+ set("a", "a", "a");
+ assertValue("a", "a", "a");
+ }
+
+ /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
+ @Test public void aggressiveClearingHandlesEdit() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Editor a = cache.get("a").edit();
+ tempDir.delete();
+ setString(a, 1, "a2");
+ a.commit();
+ }
+
+ @Test public void removeHandlesMissingFile() throws Exception {
+ set("a", "a", "a");
+ getCleanFile("a", 0).delete();
+ cache.remove("a");
+ }
+
+ /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
+ @Test public void aggressiveClearingHandlesPartialEdit() throws Exception {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ DiskLruCache.Editor a = cache.get("a").edit();
+ setString(a, 0, "a1");
+ tempDir.delete();
+ setString(a, 1, "a2");
+ a.commit();
+ assertNull(cache.get("a"));
+ }
+
+ /** @see <a href="https://github.com/JakeWharton/DiskLruCache/issues/2">Issue #2</a> */
+ @Test public void aggressiveClearingHandlesRead() throws Exception {
+ tempDir.delete();
+ assertNull(cache.get("a"));
+ }
+
+ /**
+ * We had a long-lived bug where {@link DiskLruCache#trimToSize} could
+ * infinite loop if entries being edited required deletion for the operation
+ * to complete.
+ */
+ @Test public void trimToSizeWithActiveEdit() throws Exception {
+ set("a", "a1234", "a1234");
+ DiskLruCache.Editor a = cache.edit("a");
+ setString(a, 0, "a123");
+
+ cache.setMaxSize(8); // Smaller than the sum of active edits!
+ cache.flush(); // Force trimToSize().
+ assertEquals(0, cache.size());
+ assertNull(cache.get("a"));
+
+ // After the edit is completed, its entry is still gone.
+ setString(a, 1, "a1");
+ a.commit();
+ assertAbsent("a");
+ assertEquals(0, cache.size());
+ }
+
+ @Test public void evictAll() throws Exception {
+ set("a", "a", "a");
+ set("b", "b", "b");
+ cache.evictAll();
+ assertEquals(0, cache.size());
+ assertAbsent("a");
+ assertAbsent("b");
+ }
+
+ @Test public void evictAllWithPartialCreate() throws Exception {
+ DiskLruCache.Editor a = cache.edit("a");
+ setString(a, 0, "a1");
+ setString(a, 1, "a2");
+ cache.evictAll();
+ assertEquals(0, cache.size());
+ a.commit();
+ assertAbsent("a");
+ }
+
+ @Test public void evictAllWithPartialEditDoesNotStoreAValue() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Editor a = cache.edit("a");
+ setString(a, 0, "a1");
+ setString(a, 1, "a2");
+ cache.evictAll();
+ assertEquals(0, cache.size());
+ a.commit();
+ assertAbsent("a");
+ }
+
+ @Test public void evictAllDoesntInterruptPartialRead() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Snapshot a = cache.get("a");
+ assertSnapshotValue(a, 0, "a");
+ cache.evictAll();
+ assertEquals(0, cache.size());
+ assertAbsent("a");
+ assertSnapshotValue(a, 1, "a");
+ a.close();
+ }
+
+ @Test public void editSnapshotAfterEvictAllReturnsNullDueToStaleValue() throws Exception {
+ set("a", "a", "a");
+ DiskLruCache.Snapshot a = cache.get("a");
+ cache.evictAll();
+ assertEquals(0, cache.size());
+ assertAbsent("a");
+ assertNull(a.edit());
+ a.close();
+ }
+
+ @Test public void iterator() throws Exception {
+ set("a", "a1", "a2");
+ set("b", "b1", "b2");
+ set("c", "c1", "c2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+
+ assertTrue(iterator.hasNext());
+ DiskLruCache.Snapshot a = iterator.next();
+ assertEquals("a", a.key());
+ assertSnapshotValue(a, 0, "a1");
+ assertSnapshotValue(a, 1, "a2");
+ a.close();
+
+ assertTrue(iterator.hasNext());
+ DiskLruCache.Snapshot b = iterator.next();
+ assertEquals("b", b.key());
+ assertSnapshotValue(b, 0, "b1");
+ assertSnapshotValue(b, 1, "b2");
+ b.close();
+
+ assertTrue(iterator.hasNext());
+ DiskLruCache.Snapshot c = iterator.next();
+ assertEquals("c", c.key());
+ assertSnapshotValue(c, 0, "c1");
+ assertSnapshotValue(c, 1, "c2");
+ c.close();
+
+ assertFalse(iterator.hasNext());
+ try {
+ iterator.next();
+ fail();
+ } catch (NoSuchElementException expected) {
+ }
+ }
+
+ @Test public void iteratorElementsAddedDuringIterationAreOmitted() throws Exception {
+ set("a", "a1", "a2");
+ set("b", "b1", "b2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+
+ DiskLruCache.Snapshot a = iterator.next();
+ assertEquals("a", a.key());
+ a.close();
+
+ set("c", "c1", "c2");
+
+ DiskLruCache.Snapshot b = iterator.next();
+ assertEquals("b", b.key());
+ b.close();
+
+ assertFalse(iterator.hasNext());
+ }
+
+ @Test public void iteratorElementsUpdatedDuringIterationAreUpdated() throws Exception {
+ set("a", "a1", "a2");
+ set("b", "b1", "b2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+
+ DiskLruCache.Snapshot a = iterator.next();
+ assertEquals("a", a.key());
+ a.close();
+
+ set("b", "b3", "b4");
+
+ DiskLruCache.Snapshot b = iterator.next();
+ assertEquals("b", b.key());
+ assertSnapshotValue(b, 0, "b3");
+ assertSnapshotValue(b, 1, "b4");
+ b.close();
+ }
+
+ @Test public void iteratorElementsRemovedDuringIterationAreOmitted() throws Exception {
+ set("a", "a1", "a2");
+ set("b", "b1", "b2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+
+ cache.remove("b");
+
+ DiskLruCache.Snapshot a = iterator.next();
+ assertEquals("a", a.key());
+ a.close();
+
+ assertFalse(iterator.hasNext());
+ }
+
+ @Test public void iteratorRemove() throws Exception {
+ set("a", "a1", "a2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+
+ DiskLruCache.Snapshot a = iterator.next();
+ a.close();
+ iterator.remove();
+
+ assertEquals(null, cache.get("a"));
+ }
+
+ @Test public void iteratorRemoveBeforeNext() throws Exception {
+ set("a", "a1", "a2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+ try {
+ iterator.remove();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void iteratorRemoveOncePerCallToNext() throws Exception {
+ set("a", "a1", "a2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+
+ DiskLruCache.Snapshot a = iterator.next();
+ iterator.remove();
+ a.close();
+
+ try {
+ iterator.remove();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void cacheClosedTruncatesIterator() throws Exception {
+ set("a", "a1", "a2");
+ Iterator<DiskLruCache.Snapshot> iterator = cache.snapshots();
+ cache.close();
+ assertFalse(iterator.hasNext());
+ }
+
+ @Test public void isClosed_uninitializedCache() throws Exception {
+ // Create an uninitialized cache.
+ cache = new DiskLruCache(cacheDir, appVersion, 2, Integer.MAX_VALUE, executor);
+ toClose.add(cache);
+
+ assertFalse(cache.isClosed());
+ cache.close();
+ assertTrue(cache.isClosed());
+ }
+
+ private void assertJournalEquals(String... expectedBodyLines) throws Exception {
+ List<String> expectedLines = new ArrayList<>();
+ expectedLines.add(MAGIC);
+ expectedLines.add(VERSION_1);
+ expectedLines.add("100");
+ expectedLines.add("2");
+ expectedLines.add("");
+ expectedLines.addAll(Arrays.asList(expectedBodyLines));
+ assertEquals(expectedLines, readJournalLines());
+ }
+
+ private void createJournal(String... bodyLines) throws Exception {
+ createJournalWithHeader(MAGIC, VERSION_1, "100", "2", "", bodyLines);
+ }
+
+ private void createJournalWithHeader(String magic, String version, String appVersion,
+ String valueCount, String blank, String... bodyLines) throws Exception {
+ Writer writer = new FileWriter(journalFile);
+ writer.write(magic + "\n");
+ writer.write(version + "\n");
+ writer.write(appVersion + "\n");
+ writer.write(valueCount + "\n");
+ writer.write(blank + "\n");
+ for (String line : bodyLines) {
+ writer.write(line);
+ writer.write('\n');
+ }
+ writer.close();
+ }
+
+ private List<String> readJournalLines() throws Exception {
+ List<String> result = new ArrayList<>();
+ BufferedReader reader = new BufferedReader(new FileReader(journalFile));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ result.add(line);
+ }
+ reader.close();
+ return result;
+ }
+
+ private File getCleanFile(String key, int index) {
+ return new File(cacheDir, key + "." + index);
+ }
+
+ private File getDirtyFile(String key, int index) {
+ return new File(cacheDir, key + "." + index + ".tmp");
+ }
+
+ private static String readFile(File file) throws Exception {
+ Reader reader = new FileReader(file);
+ StringWriter writer = new StringWriter();
+ char[] buffer = new char[1024];
+ int count;
+ while ((count = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, count);
+ }
+ reader.close();
+ return writer.toString();
+ }
+
+ public static void writeFile(File file, String content) throws Exception {
+ FileWriter writer = new FileWriter(file);
+ writer.write(content);
+ writer.close();
+ }
+
+ private static void assertInoperable(DiskLruCache.Editor editor) throws Exception {
+ try {
+ setString(editor, 0, "A");
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ try {
+ editor.newSource(0);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ try {
+ editor.newSink(0);
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ try {
+ editor.commit();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ try {
+ editor.abort();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ private void generateSomeGarbageFiles() throws Exception {
+ File dir1 = new File(cacheDir, "dir1");
+ File dir2 = new File(dir1, "dir2");
+ writeFile(getCleanFile("g1", 0), "A");
+ writeFile(getCleanFile("g1", 1), "B");
+ writeFile(getCleanFile("g2", 0), "C");
+ writeFile(getCleanFile("g2", 1), "D");
+ writeFile(getCleanFile("g2", 1), "D");
+ writeFile(new File(cacheDir, "otherFile0"), "E");
+ dir1.mkdir();
+ dir2.mkdir();
+ writeFile(new File(dir2, "otherFile1"), "F");
+ }
+
+ private void assertGarbageFilesAllDeleted() throws Exception {
+ assertFalse(getCleanFile("g1", 0).exists());
+ assertFalse(getCleanFile("g1", 1).exists());
+ assertFalse(getCleanFile("g2", 0).exists());
+ assertFalse(getCleanFile("g2", 1).exists());
+ assertFalse(new File(cacheDir, "otherFile0").exists());
+ assertFalse(new File(cacheDir, "dir1").exists());
+ }
+
+ private void set(String key, String value0, String value1) throws Exception {
+ DiskLruCache.Editor editor = cache.edit(key);
+ setString(editor, 0, value0);
+ setString(editor, 1, value1);
+ editor.commit();
+ }
+
+ public static void setString(DiskLruCache.Editor editor, int index, String value) throws IOException {
+ BufferedSink writer = Okio.buffer(editor.newSink(index));
+ writer.writeUtf8(value);
+ writer.close();
+ }
+
+ private void assertAbsent(String key) throws Exception {
+ DiskLruCache.Snapshot snapshot = cache.get(key);
+ if (snapshot != null) {
+ snapshot.close();
+ fail();
+ }
+ assertFalse(getCleanFile(key, 0).exists());
+ assertFalse(getCleanFile(key, 1).exists());
+ assertFalse(getDirtyFile(key, 0).exists());
+ assertFalse(getDirtyFile(key, 1).exists());
+ }
+
+ private void assertValue(String key, String value0, String value1) throws Exception {
+ DiskLruCache.Snapshot snapshot = cache.get(key);
+ assertSnapshotValue(snapshot, 0, value0);
+ assertSnapshotValue(snapshot, 1, value1);
+ assertTrue(getCleanFile(key, 0).exists());
+ assertTrue(getCleanFile(key, 1).exists());
+ snapshot.close();
+ }
+
+ private void assertSnapshotValue(DiskLruCache.Snapshot snapshot, int index, String value)
+ throws IOException {
+ assertEquals(value, sourceAsString(snapshot.getSource(index)));
+ assertEquals(value.length(), snapshot.getLength(index));
+ }
+
+ private String sourceAsString(Source source) throws IOException {
+ return source != null ? Okio.buffer(source).readUtf8() : null;
+ }
+
+ private void copyFile(File from, File to) throws IOException {
+ Source source = Okio.source(from);
+ BufferedSink sink = Okio.buffer(Okio.sink(to));
+ sink.writeAll(source);
+ source.close();
+ sink.close();
+ }
+
+ private static class TestExecutor implements Executor {
+ final Deque<Runnable> jobs = new ArrayDeque<>();
+
+ @Override public void execute(Runnable command) {
+ jobs.addLast(command);
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
new file mode 100644
index 0000000..4934b42
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * 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] };
+ }
+}
diff --git a/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
similarity index 99%
rename from android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
index c53fb21..3e129a2 100644
--- a/android/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/OptionalMethodTest.java
@@ -333,5 +333,4 @@
} catch (IllegalArgumentException expected) {
}
}
-
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
index 9eff919..292875b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingAuthenticator.java
@@ -24,7 +24,7 @@
/** base64("username:password") */
public static final String BASE_64_CREDENTIALS = "dXNlcm5hbWU6cGFzc3dvcmQ=";
- public final List<String> calls = new ArrayList<String>();
+ public final List<String> calls = new ArrayList<>();
public final PasswordAuthentication authentication;
public RecordingAuthenticator(PasswordAuthentication authentication) {
@@ -36,23 +36,14 @@
}
@Override protected PasswordAuthentication getPasswordAuthentication() {
- this.calls
- .add("host="
- + getRequestingHost()
- + " port="
- + getRequestingPort()
- + " site="
- + getRequestingSite()
- + " url="
- + getRequestingURL()
- + " type="
- + getRequestorType()
- + " prompt="
- + getRequestingPrompt()
- + " protocol="
- + getRequestingProtocol()
- + " scheme="
- + getRequestingScheme());
+ this.calls.add("host=" + getRequestingHost()
+ + " port=" + getRequestingPort()
+ + " site=" + getRequestingSite().getHostName()
+ + " url=" + getRequestingURL()
+ + " type=" + getRequestorType()
+ + " prompt=" + getRequestingPrompt()
+ + " protocol=" + getRequestingProtocol()
+ + " scheme=" + getRequestingScheme());
return authentication;
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
index b3e2369..c9d914f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingHostnameVerifier.java
@@ -21,7 +21,7 @@
import javax.net.ssl.SSLSession;
public final class RecordingHostnameVerifier implements HostnameVerifier {
- public final List<String> calls = new ArrayList<String>();
+ public final List<String> calls = new ArrayList<>();
public boolean verify(String hostname, SSLSession session) {
calls.add("verify " + hostname);
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
index 5d3020f..f5b3617 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/RecordingOkAuthenticator.java
@@ -15,31 +15,25 @@
*/
package com.squareup.okhttp.internal;
-import com.squareup.okhttp.OkAuthenticator;
-import java.io.IOException;
+import com.squareup.okhttp.Authenticator;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
import java.net.Proxy;
-import java.net.URL;
import java.util.ArrayList;
import java.util.List;
-public final class RecordingOkAuthenticator implements OkAuthenticator {
- public final List<URL> urls = new ArrayList<URL>();
- public final List<List<Challenge>> challengesList = new ArrayList<List<Challenge>>();
- public final List<Proxy> proxies = new ArrayList<Proxy>();
- public final Credential credential;
+public final class RecordingOkAuthenticator implements Authenticator {
+ public final List<Response> responses = new ArrayList<>();
+ public final List<Proxy> proxies = new ArrayList<>();
+ public final String credential;
- public RecordingOkAuthenticator(Credential credential) {
+ public RecordingOkAuthenticator(String credential) {
this.credential = credential;
}
- public URL onlyUrl() {
- if (urls.size() != 1) throw new IllegalStateException();
- return urls.get(0);
- }
-
- public List<Challenge> onlyChallenge() {
- if (challengesList.size() != 1) throw new IllegalStateException();
- return challengesList.get(0);
+ public Response onlyResponse() {
+ if (responses.size() != 1) throw new IllegalStateException();
+ return responses.get(0);
}
public Proxy onlyProxy() {
@@ -47,19 +41,19 @@
return proxies.get(0);
}
- @Override public Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges)
- throws IOException {
- urls.add(url);
- challengesList.add(challenges);
+ @Override public Request authenticate(Proxy proxy, Response response) {
+ responses.add(response);
proxies.add(proxy);
- return credential;
+ return response.request().newBuilder()
+ .addHeader("Authorization", credential)
+ .build();
}
- @Override public Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges)
- throws IOException {
- urls.add(url);
- challengesList.add(challenges);
+ @Override public Request authenticateProxy(Proxy proxy, Response response) {
+ responses.add(response);
proxies.add(proxy);
- return credential;
+ return response.request().newBuilder()
+ .addHeader("Proxy-Authorization", credential)
+ .build();
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
new file mode 100644
index 0000000..beb48cb
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2014 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;
+
+/**
+ * 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] };
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java
deleted file mode 100644
index 28c983e..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Copyright (C) 2014 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 org.junit.Test;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocket;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-public class TlsConfigurationTest {
-
- private static final SSLContext sslContext = SslContextBuilder.localhost();
-
- @Test
- public void sslV3Only() throws Exception {
- SSLSocket compatibleSocket = createSocketWithEnabledProtocols("SSLv3", "TLSv1");
- try {
- assertTrue(TlsConfiguration.SSL_V3_ONLY.isCompatible(compatibleSocket));
- TlsConfiguration.SSL_V3_ONLY.configureProtocols(compatibleSocket);
- assertEnabledProtocols(compatibleSocket, "SSLv3");
- } finally {
- compatibleSocket.close();
- }
-
- SSLSocket incompatibleSocket = createSocketWithEnabledProtocols("TLSv1");
- try {
- assertFalse(TlsConfiguration.SSL_V3_ONLY.isCompatible(incompatibleSocket));
- } finally {
- incompatibleSocket.close();
- }
-
- assertFalse(TlsConfiguration.SSL_V3_ONLY.supportsNpn());
- }
-
- @Test
- public void tlsV1AndBelow() throws Exception {
- SSLSocket compatibleSocket = createSocketWithEnabledProtocols("SSLv3", "TLSv1", "TLSv1.1");
- try {
- assertTrue(TlsConfiguration.TLS_V1_0_AND_BELOW.isCompatible(compatibleSocket));
- TlsConfiguration.TLS_V1_0_AND_BELOW.configureProtocols(compatibleSocket);
- assertEnabledProtocols(compatibleSocket, "TLSv1", "SSLv3");
- } finally {
- compatibleSocket.close();
- }
-
- compatibleSocket = createSocketWithEnabledProtocols("TLSv1", "TLSv1.1");
- try {
- assertTrue(TlsConfiguration.TLS_V1_0_AND_BELOW.isCompatible(compatibleSocket));
- TlsConfiguration.TLS_V1_0_AND_BELOW.configureProtocols(compatibleSocket);
- assertEnabledProtocols(compatibleSocket, "TLSv1");
- } finally {
- compatibleSocket.close();
- }
-
- SSLSocket incompatibleSocket = createSocketWithEnabledProtocols("TLSv1.1");
- try {
- assertFalse(TlsConfiguration.TLS_V1_0_AND_BELOW.isCompatible(incompatibleSocket));
- } finally {
- incompatibleSocket.close();
- }
-
- incompatibleSocket = createSocketWithEnabledProtocols("SSLv3");
- try {
- assertFalse(TlsConfiguration.TLS_V1_0_AND_BELOW.isCompatible(incompatibleSocket));
- } finally {
- incompatibleSocket.close();
- }
-
- assertTrue(TlsConfiguration.TLS_V1_0_AND_BELOW.supportsNpn());
- }
-
- private SSLSocket createSocketWithEnabledProtocols(String... protocols) throws IOException {
- SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
- socket.setEnabledProtocols(protocols);
- return socket;
- }
-
- private static void assertEnabledProtocols(SSLSocket socket, String... required) {
- Set<String> actual = new HashSet<String>(Arrays.asList(socket.getEnabledProtocols()));
- Set<String> expected = new HashSet<String>(Arrays.asList(required));
- assertEquals(expected, actual);
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java
deleted file mode 100644
index 1b21b68..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2014 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 org.junit.Before;
-import org.junit.Test;
-
-import java.io.IOException;
-import java.security.cert.CertificateException;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Set;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSocket;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-public class TlsFallbackStrategyTest {
-
- private static final SSLContext sslContext = SslContextBuilder.localhost();
- private static final String[] TLSV11_TLSV10_AND_SSLV3 =
- new String[] { "TLSv1.1", "TLSv1", "SSLv3" };
- private static final String[] TLSV1_ONLY = new String[] { "TLSv1" };
- public static final SSLHandshakeException RETRYABLE_EXCEPTION = new SSLHandshakeException(
- "Simulated handshake exception");
-
- private TlsFallbackStrategy fallbackStrategy;
- private Platform platform;
-
- @Before
- public void setUp() throws Exception {
- fallbackStrategy = TlsFallbackStrategy.create();
- platform = new Platform();
- }
-
- @Test
- public void nonRetryableIOException() throws Exception {
- SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV11_TLSV10_AND_SSLV3);
- try {
- fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
-
- boolean retry = fallbackStrategy.connectionFailed(new IOException("Non-handshake exception"));
- assertFalse(retry);
- } finally {
- supportsSslV3.close();
- }
- }
-
- @Test
- public void nonRetryableSSLHandshakeException() throws Exception {
- SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV11_TLSV10_AND_SSLV3);
- try {
- fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
-
- SSLHandshakeException trustIssueException =
- new SSLHandshakeException("Certificate handshake exception",
- new CertificateException());
- boolean retry = fallbackStrategy.connectionFailed(trustIssueException);
- assertFalse(retry);
- } finally {
- supportsSslV3.close();
- }
- }
-
- @Test
- public void retryableSSLHandshakeException() throws Exception {
- SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV11_TLSV10_AND_SSLV3);
- try {
- fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
-
- boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
- assertTrue(retry);
- } finally {
- supportsSslV3.close();
- }
- }
-
- @Test
- public void someFallbacksSupported() throws Exception {
- SSLSocket socket = createSocketWithEnabledProtocols(TLSV11_TLSV10_AND_SSLV3);
- try {
- fallbackStrategy.configureSecureSocket(socket, "host", platform);
- assertEnabledProtocols(socket, TLSV11_TLSV10_AND_SSLV3);
-
- boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
- assertTrue(retry);
- } finally {
- socket.close();
- }
-
- socket = createSocketWithEnabledProtocols(TLSV11_TLSV10_AND_SSLV3);
- try {
- fallbackStrategy.configureSecureSocket(socket, "host", platform);
- assertEnabledProtocols(socket, "TLSv1", "SSLv3");
-
- boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
- assertTrue(retry);
- } finally {
- socket.close();
- }
-
- socket = createSocketWithEnabledProtocols(TLSV11_TLSV10_AND_SSLV3);
- try {
- fallbackStrategy.configureSecureSocket(socket, "host", platform);
- assertEnabledProtocols(socket, "SSLv3");
-
- boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
- assertFalse(retry);
- } finally {
- socket.close();
- }
- }
-
- @Test
- public void sslV3NotSupported() throws Exception {
- SSLSocket socket = createSocketWithEnabledProtocols(TLSV1_ONLY);
- try {
- fallbackStrategy.configureSecureSocket(socket, "host", platform);
- assertEnabledProtocols(socket, TLSV1_ONLY);
-
- boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
- assertFalse(retry);
- } finally {
- socket.close();
- }
- }
-
- private SSLSocket createSocketWithEnabledProtocols(String... protocols) throws IOException {
- SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
- socket.setEnabledProtocols(protocols);
- return socket;
- }
-
- private static void assertEnabledProtocols(SSLSocket socket, String... required) {
- Set<String> actual = new HashSet<String>(Arrays.asList(socket.getEnabledProtocols()));
- Set<String> expected = new HashSet<String>(Arrays.asList(required));
- assertEquals(expected, actual);
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
index a44e683..d0fa1b2 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/CookiesTest.java
@@ -17,14 +17,10 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
import java.io.IOException;
import java.net.CookieHandler;
import java.net.CookieManager;
@@ -37,10 +33,14 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
import static java.net.CookiePolicy.ACCEPT_ORIGINAL_SERVER;
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;
@@ -64,7 +64,7 @@
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
MockWebServer server = new MockWebServer();
- server.play();
+ server.start();
server.enqueue(new MockResponse().addHeader("Set-Cookie: a=android; "
+ "expires=Fri, 31-Dec-9999 23:59:59 GMT; "
@@ -81,7 +81,7 @@
assertEquals(null, cookie.getComment());
assertEquals(null, cookie.getCommentURL());
assertEquals(false, cookie.getDiscard());
- assertEquals(server.getCookieDomain(), cookie.getDomain());
+ assertTrue(server.getCookieDomain().equalsIgnoreCase(cookie.getDomain()));
assertTrue(cookie.getMaxAge() > 100000000000L);
assertEquals("/path", cookie.getPath());
assertEquals(true, cookie.getSecure());
@@ -92,7 +92,7 @@
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
MockWebServer server = new MockWebServer();
- server.play();
+ server.start();
server.enqueue(new MockResponse().addHeader("Set-Cookie: a=android; "
+ "Comment=this cookie is delicious; "
@@ -111,7 +111,7 @@
assertEquals("this cookie is delicious", cookie.getComment());
assertEquals(null, cookie.getCommentURL());
assertEquals(false, cookie.getDiscard());
- assertEquals(server.getCookieDomain(), cookie.getDomain());
+ assertTrue(server.getCookieDomain().equalsIgnoreCase(cookie.getDomain()));
assertEquals(60, cookie.getMaxAge());
assertEquals("/path", cookie.getPath());
assertEquals(true, cookie.getSecure());
@@ -122,7 +122,7 @@
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
MockWebServer server = new MockWebServer();
- server.play();
+ server.start();
server.enqueue(new MockResponse().addHeader("Set-Cookie2: a=android; "
+ "Comment=this cookie is delicious; "
@@ -144,7 +144,7 @@
assertEquals("this cookie is delicious", cookie.getComment());
assertEquals("http://google.com/", cookie.getCommentURL());
assertEquals(true, cookie.getDiscard());
- assertEquals(server.getCookieDomain(), cookie.getDomain());
+ assertTrue(server.getCookieDomain().equalsIgnoreCase(cookie.getDomain()));
assertEquals(60, cookie.getMaxAge());
assertEquals("/path", cookie.getPath());
assertEquals("80,443," + server.getPort(), cookie.getPortlist());
@@ -156,7 +156,7 @@
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
CookieHandler.setDefault(cookieManager);
MockWebServer server = new MockWebServer();
- server.play();
+ server.start();
server.enqueue(new MockResponse().addHeader("Set-Cookie2: a=\"android\"; "
+ "Comment=\"this cookie is delicious\"; "
@@ -178,7 +178,7 @@
assertEquals("this cookie is delicious", cookie.getComment());
assertEquals("http://google.com/", cookie.getCommentURL());
assertEquals(true, cookie.getDiscard());
- assertEquals(server.getCookieDomain(), cookie.getDomain());
+ assertTrue(server.getCookieDomain().equalsIgnoreCase(cookie.getDomain()));
assertEquals(60, cookie.getMaxAge());
assertEquals("/path", cookie.getPath());
assertEquals("80,443," + server.getPort(), cookie.getPortlist());
@@ -189,7 +189,7 @@
@Test public void testSendingCookiesFromStore() throws Exception {
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
- server.play();
+ server.start();
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
HttpCookie cookieA = new HttpCookie("a", "android");
@@ -205,22 +205,25 @@
get(server, "/");
RecordedRequest request = server.takeRequest();
- List<String> receivedHeaders = request.getHeaders();
- assertContains(receivedHeaders, "Cookie: $Version=\"1\"; "
- + "a=\"android\";$Path=\"/\";$Domain=\"" + server.getCookieDomain() + "\"; "
- + "b=\"banana\";$Path=\"/\";$Domain=\"" + server.getCookieDomain() + "\"");
+ assertEquals("$Version=\"1\"; "
+ + "a=\"android\";$Path=\"/\";$Domain=\""
+ + server.getCookieDomain()
+ + "\"; "
+ + "b=\"banana\";$Path=\"/\";$Domain=\""
+ + server.getCookieDomain()
+ + "\"", request.getHeader("Cookie"));
}
@Test public void testRedirectsDoNotIncludeTooManyCookies() throws Exception {
MockWebServer redirectTarget = new MockWebServer();
redirectTarget.enqueue(new MockResponse().setBody("A"));
- redirectTarget.play();
+ redirectTarget.start();
MockWebServer redirectSource = new MockWebServer();
redirectSource.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + redirectTarget.getUrl("/")));
- redirectSource.play();
+ redirectSource.start();
CookieManager cookieManager = new CookieManager(null, ACCEPT_ORIGINAL_SERVER);
HttpCookie cookie = new HttpCookie("c", "cookie");
@@ -234,11 +237,14 @@
get(redirectSource, "/");
RecordedRequest request = redirectSource.takeRequest();
- assertContains(request.getHeaders(), "Cookie: $Version=\"1\"; "
- + "c=\"cookie\";$Path=\"/\";$Domain=\"" + redirectSource.getCookieDomain()
- + "\";$Port=\"" + portList + "\"");
+ assertEquals("$Version=\"1\"; "
+ + "c=\"cookie\";$Path=\"/\";$Domain=\""
+ + redirectSource.getCookieDomain()
+ + "\";$Port=\""
+ + portList
+ + "\"", request.getHeader("Cookie"));
- for (String header : redirectTarget.takeRequest().getHeaders()) {
+ for (String header : redirectTarget.takeRequest().getHeaders().names()) {
if (header.startsWith("Cookie")) {
fail(header);
}
@@ -253,13 +259,13 @@
* getRequestProperties}.
*/
@Test public void testHeadersSentToCookieHandler() throws IOException, InterruptedException {
- final Map<String, List<String>> cookieHandlerHeaders = new HashMap<String, List<String>>();
+ final Map<String, List<String>> cookieHandlerHeaders = new HashMap<>();
CookieHandler.setDefault(new CookieManager() {
@Override
public Map<String, List<String>> get(URI uri,
Map<String, List<String>> requestHeaders) throws IOException {
cookieHandlerHeaders.putAll(requestHeaders);
- Map<String, List<String>> result = new HashMap<String, List<String>>();
+ Map<String, List<String>> result = new HashMap<>();
result.put("Cookie", Collections.singletonList("Bar=bar"));
result.put("Cookie2", Collections.singletonList("Baz=baz"));
result.put("Quux", Collections.singletonList("quux"));
@@ -268,9 +274,9 @@
});
MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse());
- server.play();
+ server.start();
- HttpURLConnection connection = client.open(server.getUrl("/"));
+ HttpURLConnection connection = new OkUrlFactory(client).open(server.getUrl("/"));
assertEquals(Collections.<String, List<String>>emptyMap(),
connection.getRequestProperties());
@@ -301,15 +307,17 @@
} catch (IllegalStateException expected) {
}
- assertContainsAll(request.getHeaders(), "Foo: foo", "Cookie: Bar=bar", "Cookie2: Baz=baz");
- assertFalse(request.getHeaders().contains("Quux: quux"));
+ assertEquals("foo", request.getHeader("Foo"));
+ assertEquals("Bar=bar", request.getHeader("Cookie"));
+ assertEquals("Baz=baz", request.getHeader("Cookie2"));
+ assertNull(request.getHeader("Quux"));
}
@Test public void testCookiesSentIgnoresCase() throws Exception {
CookieHandler.setDefault(new CookieManager() {
@Override public Map<String, List<String>> get(URI uri,
Map<String, List<String>> requestHeaders) throws IOException {
- Map<String, List<String>> result = new HashMap<String, List<String>>();
+ Map<String, List<String>> result = new HashMap<>();
result.put("COOKIE", Collections.singletonList("Bar=bar"));
result.put("cooKIE2", Collections.singletonList("Baz=baz"));
return result;
@@ -317,13 +325,14 @@
});
MockWebServer server = new MockWebServer();
server. enqueue(new MockResponse());
- server.play();
+ server.start();
get(server, "/");
RecordedRequest request = server.takeRequest();
- assertContainsAll(request.getHeaders(), "COOKIE: Bar=bar", "cooKIE2: Baz=baz");
- assertFalse(request.getHeaders().contains("Quux: quux"));
+ assertEquals("Bar=bar", request.getHeader("Cookie"));
+ assertEquals("Baz=baz", request.getHeader("Cookie2"));
+ assertNull(request.getHeader("Quux"));
}
private void assertContains(Collection<String> collection, String element) {
@@ -342,7 +351,7 @@
}
private Map<String,List<String>> get(MockWebServer server, String path) throws Exception {
- URLConnection connection = client.open(server.getUrl(path));
+ URLConnection connection = new OkUrlFactory(client).open(server.getUrl(path));
Map<String, List<String>> headers = connection.getHeaderFields();
connection.getInputStream().close();
return headers;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
index db84214..7a70d03 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/DisconnectTest.java
@@ -15,30 +15,67 @@
*/
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.DelegatingServerSocketFactory;
+import com.squareup.okhttp.DelegatingSocketFactory;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
+import java.net.ServerSocket;
+import java.net.Socket;
import java.util.concurrent.TimeUnit;
+
+import okio.Buffer;
+import org.junit.Before;
import org.junit.Test;
+import javax.net.ServerSocketFactory;
+import javax.net.SocketFactory;
+
import static org.junit.Assert.fail;
public final class DisconnectTest {
- private final MockWebServer server = new MockWebServer();
- private final OkHttpClient client = new OkHttpClient();
+
+ // The size of the socket buffers in bytes.
+ private static final int SOCKET_BUFFER_SIZE = 256 * 1024;
+
+ private MockWebServer server;
+ private OkHttpClient client;
+
+ @Before public void setUp() throws Exception {
+ server = new MockWebServer();
+ client = new OkHttpClient();
+
+ // Sockets on some platforms can have large buffers that mean writes do not block when
+ // required. These socket factories explicitly set the buffer sizes on sockets created.
+ server.setServerSocketFactory(
+ new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
+ @Override
+ protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+ serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ }
+ });
+ client.setSocketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
+ @Override
+ protected void configureSocket(Socket socket) throws IOException {
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ }
+ });
+ }
@Test public void interruptWritingRequestBody() throws Exception {
int requestBodySize = 2 * 1024 * 1024; // 2 MiB
server.enqueue(new MockResponse()
.throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
- server.play();
+ server.start();
- HttpURLConnection connection = client.open(server.getUrl("/"));
+ HttpURLConnection connection = new OkUrlFactory(client).open(server.getUrl("/"));
disconnectLater(connection, 500);
connection.setDoOutput(true);
@@ -61,11 +98,11 @@
int responseBodySize = 2 * 1024 * 1024; // 2 MiB
server.enqueue(new MockResponse()
- .setBody(new byte[responseBodySize])
+ .setBody(new Buffer().write(new byte[responseBodySize]))
.throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
- server.play();
+ server.start();
- HttpURLConnection connection = client.open(server.getUrl("/"));
+ HttpURLConnection connection = new OkUrlFactory(client).open(server.getUrl("/"));
disconnectLater(connection, 500);
InputStream responseBody = connection.getInputStream();
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java
index 1c5198c..020c7f0 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalHttp2Example.java
@@ -17,7 +17,9 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.Util;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
@@ -30,9 +32,11 @@
public final class ExternalHttp2Example {
public static void main(String[] args) throws Exception {
- URL url = new URL("https://http2.iijplus.jp/push/test1");
- HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient()
- .setProtocols(Protocol.HTTP2_AND_HTTP_11).open(url);
+ URL url = new URL("https://twitter.com");
+ OkHttpClient client = new OkHttpClient()
+ .setProtocols(Util.immutableList(Protocol.HTTP_2, Protocol.HTTP_1_1));
+ HttpsURLConnection connection = (HttpsURLConnection) new OkUrlFactory(client)
+ .open(url);
connection.setHostnameVerifier(new HostnameVerifier() {
@Override public boolean verify(String s, SSLSession sslSession) {
@@ -44,7 +48,7 @@
int responseCode = connection.getResponseCode();
System.out.println(responseCode);
List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
- // If null, probably you didn't add jetty's npn jar to your boot classpath!
+ // If null, probably you didn't add jetty's alpn jar to your boot classpath!
if (protocolValues != null && !protocolValues.isEmpty()) {
System.out.println("PROTOCOL " + protocolValues.get(0));
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
index dab90c1..a800962 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ExternalSpdyExample.java
@@ -17,7 +17,9 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.Util;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
@@ -31,8 +33,10 @@
public final class ExternalSpdyExample {
public static void main(String[] args) throws Exception {
URL url = new URL("https://www.google.ca/");
- HttpsURLConnection connection = (HttpsURLConnection) new OkHttpClient()
- .setProtocols(Protocol.SPDY3_AND_HTTP11).open(url);
+ OkHttpClient client = new OkHttpClient()
+ .setProtocols(Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1));
+ HttpsURLConnection connection = (HttpsURLConnection) new OkUrlFactory(client)
+ .open(url);
connection.setHostnameVerifier(new HostnameVerifier() {
@Override public boolean verify(String s, SSLSession sslSession) {
@@ -44,7 +48,7 @@
int responseCode = connection.getResponseCode();
System.out.println(responseCode);
List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
- // If null, probably you didn't add jetty's npn jar to your boot classpath!
+ // If null, probably you didn't add jetty's alpn jar to your boot classpath!
if (protocolValues != null && !protocolValues.isEmpty()) {
System.out.println("PROTOCOL " + protocolValues.get(0));
}
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 80db747..1d94622 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
@@ -21,12 +21,18 @@
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.spdy.Header;
import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+
import org.junit.Test;
-import static com.squareup.okhttp.internal.Util.headerEntries;
+import static com.squareup.okhttp.TestUtil.headerEntries;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
public final class HeadersTest {
@Test public void parseNameValueBlock() throws IOException {
@@ -40,12 +46,14 @@
SpdyTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
Headers headers = response.headers();
assertEquals(4, headers.size());
- assertEquals("HTTP/1.1 200 OK", response.statusLine());
+ 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.name.utf8(), headers.get(OkHeaders.SELECTED_PROTOCOL));
+ assertEquals(Protocol.SPDY_3.toString(), headers.get(OkHeaders.SELECTED_PROTOCOL));
assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.SPDY_3.name.utf8(), headers.value(0));
+ assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
assertEquals("cache-control", headers.name(1));
assertEquals("no-cache, no-store", headers.value(1));
assertEquals("set-cookie", headers.name(2));
@@ -67,7 +75,7 @@
Headers headers = response.headers();
assertEquals(1, headers.size());
assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.SPDY_3.name.utf8(), headers.value(0));
+ assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
}
@Test public void readNameValueBlockDropsForbiddenHeadersHttp2() throws IOException {
@@ -81,7 +89,7 @@
Headers headers = response.headers();
assertEquals(1, headers.size());
assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.HTTP_2.name.utf8(), headers.value(0));
+ assertEquals(Protocol.HTTP_2.toString(), headers.value(0));
}
@Test public void toNameValueBlock() {
@@ -135,4 +143,163 @@
assertEquals(expected,
SpdyTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
}
+
+ @Test public void ofTrims() {
+ Headers headers = Headers.of("\t User-Agent \n", " \r OkHttp ");
+ assertEquals("User-Agent", headers.name(0));
+ assertEquals("OkHttp", headers.value(0));
+ }
+
+ @Test public void addParsing() {
+ Headers headers = new Headers.Builder()
+ .add("foo: bar")
+ .add(" foo: baz") // Name leading whitespace is trimmed.
+ .add("foo : bak") // Name trailing whitespace is trimmed.
+ .add("ping: pong ") // Value whitespace is trimmed.
+ .add("kit:kat") // Space after colon is not required.
+ .build();
+ assertEquals(Arrays.asList("bar", "baz", "bak"), headers.values("foo"));
+ assertEquals(Arrays.asList("pong"), headers.values("ping"));
+ assertEquals(Arrays.asList("kat"), headers.values("kit"));
+ }
+
+ @Test public void addThrowsOnEmptyName() {
+ try {
+ new Headers.Builder().add(": bar");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ try {
+ new Headers.Builder().add(" : bar");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void addThrowsOnNoColon() {
+ try {
+ new Headers.Builder().add("foo bar");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void addThrowsOnMultiColon() {
+ try {
+ new Headers.Builder().add(":status: 200 OK");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofThrowsOddNumberOfHeaders() {
+ try {
+ Headers.of("User-Agent", "OkHttp", "Content-Length");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofThrowsOnNull() {
+ try {
+ Headers.of("User-Agent", null);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofThrowsOnEmptyName() {
+ try {
+ Headers.of("", "OkHttp");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofAcceptsEmptyValue() {
+ Headers headers = Headers.of("User-Agent", "");
+ assertEquals("", headers.value(0));
+ }
+
+ @Test public void ofMakesDefensiveCopy() {
+ String[] namesAndValues = {
+ "User-Agent",
+ "OkHttp"
+ };
+ Headers headers = Headers.of(namesAndValues);
+ namesAndValues[1] = "Chrome";
+ assertEquals("OkHttp", headers.value(0));
+ }
+
+ @Test public void ofRejectsNulChar() {
+ try {
+ Headers.of("User-Agent", "Square\u0000OkHttp");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofMapThrowsOnNull() {
+ try {
+ Headers.of(Collections.<String, String>singletonMap("User-Agent", null));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofMapThrowsOnEmptyName() {
+ try {
+ Headers.of(Collections.singletonMap("", "OkHttp"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofMapThrowsOnBlankName() {
+ try {
+ Headers.of(Collections.singletonMap(" ", "OkHttp"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofMapAcceptsEmptyValue() {
+ Headers headers = Headers.of(Collections.singletonMap("User-Agent", ""));
+ assertEquals("", headers.value(0));
+ }
+
+ @Test public void ofMapTrimsKey() {
+ Headers headers = Headers.of(Collections.singletonMap(" User-Agent ", "OkHttp"));
+ assertEquals("User-Agent", headers.name(0));
+ }
+
+ @Test public void ofMapTrimsValue() {
+ Headers headers = Headers.of(Collections.singletonMap("User-Agent", " OkHttp "));
+ assertEquals("OkHttp", headers.value(0));
+ }
+
+ @Test public void ofMapMakesDefensiveCopy() {
+ Map<String, String> namesAndValues = new HashMap<>();
+ namesAndValues.put("User-Agent", "OkHttp");
+
+ Headers headers = Headers.of(namesAndValues);
+ namesAndValues.put("User-Agent", "Chrome");
+ assertEquals("OkHttp", headers.value(0));
+ }
+
+ @Test public void ofMapRejectsNulCharInName() {
+ try {
+ Headers.of(Collections.singletonMap("User-Agent", "Square\u0000OkHttp"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+
+ @Test public void ofMapRejectsNulCharInValue() {
+ try {
+ Headers.of(Collections.singletonMap("User-\u0000Agent", "OkHttp"));
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft16Test.java
similarity index 64%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft16Test.java
index 851a9c1..7659110 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft09Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp20Draft16Test.java
@@ -15,28 +15,30 @@
*/
package com.squareup.okhttp.internal.http;
+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.util.Arrays;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
-public class HttpOverHttp20Draft09Test extends HttpOverSpdyTest {
+public class HttpOverHttp20Draft16Test extends HttpOverSpdyTest {
- public HttpOverHttp20Draft09Test() {
+ public HttpOverHttp20Draft16Test() {
super(Protocol.HTTP_2);
this.hostHeader = ":authority";
}
@Test public void serverSendsPushPromise_GET() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet")
- .withPush(new PushPromise("GET", "/foo/bar", Arrays.asList("foo: bar"),
- new MockResponse().setBody("bar").setStatus("HTTP/1.1 200 Sweet")));
+ PushPromise pushPromise = new PushPromise("GET", "/foo/bar", Headers.of("foo", "bar"),
+ new MockResponse().setBody("bar").setStatus("HTTP/1.1 200 Sweet"));
+ MockResponse response = new MockResponse()
+ .setBody("ABCDE")
+ .setStatus("HTTP/1.1 200 Sweet")
+ .withPush(pushPromise);
server.enqueue(response);
- server.play();
connection = client.open(server.getUrl("/foo"));
assertContent("ABCDE", connection, Integer.MAX_VALUE);
@@ -45,20 +47,22 @@
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(), ":scheme: https");
- assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort());
+ assertEquals("https", request.getHeader(":scheme"));
+ assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(hostHeader));
RecordedRequest pushedRequest = server.takeRequest();
assertEquals("GET /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
- assertEquals(Arrays.asList("foo: bar"), pushedRequest.getHeaders());
+ assertEquals("bar", pushedRequest.getHeader("foo"));
}
@Test public void serverSendsPushPromise_HEAD() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet")
- .withPush(new PushPromise("HEAD", "/foo/bar", Arrays.asList("foo: bar"),
- new MockResponse().setStatus("HTTP/1.1 204 Sweet")));
+ PushPromise pushPromise = new PushPromise("HEAD", "/foo/bar", Headers.of("foo", "bar"),
+ new MockResponse().setStatus("HTTP/1.1 204 Sweet"));
+ MockResponse response = new MockResponse()
+ .setBody("ABCDE")
+ .setStatus("HTTP/1.1 200 Sweet")
+ .withPush(pushPromise);
server.enqueue(response);
- server.play();
connection = client.open(server.getUrl("/foo"));
assertContent("ABCDE", connection, Integer.MAX_VALUE);
@@ -67,11 +71,11 @@
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(), ":scheme: https");
- assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort());
+ assertEquals("https", request.getHeader(":scheme"));
+ assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(hostHeader));
RecordedRequest pushedRequest = server.takeRequest();
assertEquals("HEAD /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
- assertEquals(Arrays.asList("foo: bar"), pushedRequest.getHeaders());
+ assertEquals("bar", pushedRequest.getHeader("foo"));
}
}
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/http/HttpOverSpdyTest.java
index c725a75..ab8f3c9 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
@@ -15,60 +15,55 @@
*/
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.HttpResponseCache;
+import com.squareup.okhttp.Cache;
+import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.internal.RecordingAuthenticator;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.mockwebserver.MockResponse;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import java.io.IOException;
import java.io.InputStream;
-import java.io.OutputStream;
import java.net.Authenticator;
import java.net.CookieManager;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
-import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
-import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.zip.GZIPOutputStream;
+import java.util.concurrent.TimeUnit;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.GzipSink;
+import okio.Okio;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/** Test how SPDY interacts with HTTP features. */
public abstract class HttpOverSpdyTest {
-
- /** Protocol to test, for example {@link com.squareup.okhttp.Protocol#SPDY_3} */
- private final Protocol protocol;
- protected String hostHeader = ":host";
-
- protected HttpOverSpdyTest(Protocol protocol){
- this.protocol = protocol;
- }
+ private static final SSLContext sslContext = SslContextBuilder.localhost();
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) {
@@ -76,32 +71,36 @@
}
};
- private static final SSLContext sslContext = SslContextBuilder.localhost();
- protected final MockWebServer server = new MockWebServer();
- protected final String hostName = server.getHostName();
- protected final OkHttpClient client = new OkHttpClient();
+ @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
+ @Rule public final MockWebServerRule server = new MockWebServerRule();
+
+ /** Protocol to test, for example {@link com.squareup.okhttp.Protocol#SPDY_3} */
+ private final Protocol protocol;
+ protected String hostHeader = ":host";
+
+ protected final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
protected HttpURLConnection connection;
- protected HttpResponseCache cache;
+ protected Cache cache;
+
+ protected HttpOverSpdyTest(Protocol protocol){
+ this.protocol = protocol;
+ }
@Before public void setUp() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
- client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_11));
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
- String systemTmpDir = System.getProperty("java.io.tmpdir");
- File cacheDir = new File(systemTmpDir, "HttpCache-" + protocol + "-" + UUID.randomUUID());
- cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ client.client().setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+ cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE);
}
@After public void tearDown() throws Exception {
Authenticator.setDefault(null);
- server.shutdown();
}
@Test public void get() throws Exception {
MockResponse response = new MockResponse().setBody("ABCDE").setStatus("HTTP/1.1 200 Sweet");
server.enqueue(response);
- server.play();
connection = client.open(server.getUrl("/foo"));
assertContent("ABCDE", connection, Integer.MAX_VALUE);
@@ -110,13 +109,12 @@
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(), ":scheme: https");
- assertContains(request.getHeaders(), hostHeader + ": " + hostName + ":" + server.getPort());
+ assertEquals("https", request.getHeader(":scheme"));
+ assertEquals(server.getHostName() + ":" + server.getPort(), request.getHeader(hostHeader));
}
@Test public void emptyResponse() throws IOException {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/foo"));
assertEquals(-1, connection.getInputStream().read());
@@ -124,27 +122,23 @@
byte[] postBytes = "FGHIJ".getBytes(Util.UTF_8);
- /** An output stream can be written to more than once, so we can't guess content length. */
- @Test public void noDefaultContentLengthOnPost() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDE");
- server.enqueue(response);
- server.play();
+ @Test public void noDefaultContentLengthOnStreamingPost() throws Exception {
+ server.enqueue(new MockResponse().setBody("ABCDE"));
connection = client.open(server.getUrl("/foo"));
connection.setDoOutput(true);
+ connection.setChunkedStreamingMode(0);
connection.getOutputStream().write(postBytes);
assertContent("ABCDE", connection, Integer.MAX_VALUE);
RecordedRequest request = server.takeRequest();
assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
- assertArrayEquals(postBytes, request.getBody());
+ assertArrayEquals(postBytes, request.getBody().readByteArray());
assertNull(request.getHeader("Content-Length"));
}
@Test public void userSuppliedContentLengthHeader() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDE");
- server.enqueue(response);
- server.play();
+ server.enqueue(new MockResponse().setBody("ABCDE"));
connection = client.open(server.getUrl("/foo"));
connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length));
@@ -154,14 +148,12 @@
RecordedRequest request = server.takeRequest();
assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
- assertArrayEquals(postBytes, request.getBody());
+ assertArrayEquals(postBytes, request.getBody().readByteArray());
assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length")));
}
@Test public void closeAfterFlush() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDE");
- server.enqueue(response);
- server.play();
+ server.enqueue(new MockResponse().setBody("ABCDE"));
connection = client.open(server.getUrl("/foo"));
connection.setRequestProperty("Content-Length", String.valueOf(postBytes.length));
@@ -173,14 +165,12 @@
RecordedRequest request = server.takeRequest();
assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
- assertArrayEquals(postBytes, request.getBody());
+ assertArrayEquals(postBytes, request.getBody().readByteArray());
assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length")));
}
@Test public void setFixedLengthStreamingModeSetsContentLength() throws Exception {
- MockResponse response = new MockResponse().setBody("ABCDE");
- server.enqueue(response);
- server.play();
+ server.enqueue(new MockResponse().setBody("ABCDE"));
connection = client.open(server.getUrl("/foo"));
connection.setFixedLengthStreamingMode(postBytes.length);
@@ -190,14 +180,13 @@
RecordedRequest request = server.takeRequest();
assertEquals("POST /foo HTTP/1.1", request.getRequestLine());
- assertArrayEquals(postBytes, request.getBody());
+ assertArrayEquals(postBytes, request.getBody().readByteArray());
assertEquals(postBytes.length, Integer.parseInt(request.getHeader("Content-Length")));
}
@Test public void spdyConnectionReuse() throws Exception {
server.enqueue(new MockResponse().setBody("ABCDEF"));
server.enqueue(new MockResponse().setBody("GHIJKL"));
- server.play();
HttpURLConnection connection1 = client.open(server.getUrl("/r1"));
HttpURLConnection connection2 = client.open(server.getUrl("/r2"));
@@ -212,7 +201,6 @@
@Test @Ignore public void synchronousSpdyRequest() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
server.enqueue(new MockResponse().setBody("A"));
- server.play();
ExecutorService executor = Executors.newCachedThreadPool();
CountDownLatch countDownLatch = new CountDownLatch(2);
@@ -224,9 +212,8 @@
}
@Test public void gzippedResponseBody() throws Exception {
- server.enqueue(new MockResponse().addHeader("Content-Encoding: gzip")
- .setBody(gzip("ABCABCABC".getBytes(Util.UTF_8))));
- server.play();
+ server.enqueue(
+ new MockResponse().addHeader("Content-Encoding: gzip").setBody(gzip("ABCABCABC")));
assertContent("ABCABCABC", client.open(server.getUrl("/r1")), Integer.MAX_VALUE);
}
@@ -235,18 +222,17 @@
.addHeader("www-authenticate: Basic realm=\"protected area\"")
.setBody("Please authenticate."));
server.enqueue(new MockResponse().setBody("Successful auth!"));
- server.play();
Authenticator.setDefault(new RecordingAuthenticator());
connection = client.open(server.getUrl("/"));
assertEquals("Successful auth!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest denied = server.takeRequest();
- assertContainsNoneMatching(denied.getHeaders(), "authorization: Basic .*");
+ assertNull(denied.getHeader("Authorization"));
RecordedRequest accepted = server.takeRequest();
assertEquals("GET / HTTP/1.1", accepted.getRequestLine());
- assertContains(accepted.getHeaders(),
- "authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+ assertEquals("Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS,
+ accepted.getHeader("Authorization"));
}
@Test public void redirect() throws Exception {
@@ -254,7 +240,6 @@
.addHeader("Location: /foo")
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("This is the new location!"));
- server.play();
connection = client.open(server.getUrl("/"));
assertContent("This is the new location!", connection, Integer.MAX_VALUE);
@@ -267,7 +252,6 @@
@Test public void readAfterLastByte() throws Exception {
server.enqueue(new MockResponse().setBody("ABC"));
- server.play();
connection = client.open(server.getUrl("/"));
InputStream in = connection.getInputStream();
@@ -280,7 +264,6 @@
@Test(timeout = 3000) public void readResponseHeaderTimeout() throws Exception {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
connection.setReadTimeout(1000);
@@ -297,10 +280,7 @@
@Test public void readTimeoutMoreGranularThanBodySize() throws Exception {
char[] body = new char[4096]; // 4KiB to read
Arrays.fill(body, 'y');
- server.enqueue(new MockResponse()
- .setBody(new String(body))
- .throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second
- server.play();
+ server.enqueue(new MockResponse().setBody(new String(body)).throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second
connection = client.open(server.getUrl("/"));
connection.setReadTimeout(2000); // 2 seconds to read something.
@@ -320,7 +300,6 @@
server.enqueue(new MockResponse()
.setBody(new String(body))
.throttleBody(1024, 1, SECONDS)); // slow connection 1KiB/second
- server.play();
connection = client.open(server.getUrl("/"));
connection.setReadTimeout(500); // half a second to read something
@@ -329,15 +308,14 @@
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail("Should have timed out!");
} catch (IOException e){
- assertEquals("Read timed out", e.getMessage());
+ assertEquals("timeout", e.getMessage());
}
}
@Test public void spdyConnectionTimeout() throws Exception {
MockResponse response = new MockResponse().setBody("A");
- response.setBodyDelayTimeMs(1000);
+ response.setBodyDelay(1, TimeUnit.SECONDS);
server.enqueue(response);
- server.play();
HttpURLConnection connection1 = client.open(server.getUrl("/"));
connection1.setReadTimeout(2000);
@@ -349,10 +327,9 @@
}
@Test public void responsesAreCached() throws IOException {
- client.setOkResponseCache(cache);
+ client.client().setCache(cache);
server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("A"));
- server.play();
assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
assertEquals(1, cache.getRequestCount());
@@ -366,11 +343,10 @@
}
@Test public void conditionalCache() throws IOException {
- client.setOkResponseCache(cache);
+ client.client().setCache(cache);
server.enqueue(new MockResponse().addHeader("ETag: v1").setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
assertContent("A", client.open(server.getUrl("/")), Integer.MAX_VALUE);
assertEquals(1, cache.getRequestCount());
@@ -383,11 +359,10 @@
}
@Test public void responseCachedWithoutConsumingFullBody() throws IOException {
- client.setOkResponseCache(cache);
+ client.client().setCache(cache);
server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("ABCD"));
server.enqueue(new MockResponse().addHeader("cache-control: max-age=60").setBody("EFGH"));
- server.play();
HttpURLConnection connection1 = client.open(server.getUrl("/"));
InputStream in1 = connection1.getInputStream();
@@ -402,12 +377,13 @@
@Test public void acceptAndTransmitCookies() throws Exception {
CookieManager cookieManager = new CookieManager();
- client.setCookieHandler(cookieManager);
- server.enqueue(
- new MockResponse().addHeader("set-cookie: c=oreo; domain=" + server.getCookieDomain())
- .setBody("A"));
- server.enqueue(new MockResponse().setBody("B"));
- server.play();
+ client.client().setCookieHandler(cookieManager);
+
+ server.enqueue(new MockResponse()
+ .addHeader("set-cookie: c=oreo; domain=" + server.get().getCookieDomain())
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
URL url = server.getUrl("/");
assertContent("A", client.open(url), Integer.MAX_VALUE);
@@ -417,13 +393,28 @@
assertContent("B", client.open(url), Integer.MAX_VALUE);
RecordedRequest requestA = server.takeRequest();
- assertContainsNoneMatching(requestA.getHeaders(), "Cookie.*");
+ assertNull(requestA.getHeader("Cookie"));
RecordedRequest requestB = server.takeRequest();
- assertContains(requestB.getHeaders(), "cookie: c=oreo");
+ assertEquals("c=oreo", requestB.getHeader("Cookie"));
}
- <T> void assertContains(Collection<T> collection, T value) {
- assertTrue(collection.toString(), collection.contains(value));
+ /** https://github.com/square/okhttp/issues/1191 */
+ @Test public void disconnectWithStreamNotEstablished() throws Exception {
+ ConnectionPool connectionPool = new ConnectionPool(5, 5000);
+ client.client().setConnectionPool(connectionPool);
+
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ // Disconnect before the stream is created. A connection is still established!
+ HttpURLConnection connection1 = client.open(server.getUrl("/"));
+ connection1.connect();
+ connection1.disconnect();
+
+ // That connection is pooled, and it works.
+ assertEquals(1, connectionPool.getSpdyConnectionCount());
+ HttpURLConnection connection2 = client.open(server.getUrl("/"));
+ assertContent("abc", connection2, 3);
+ assertEquals(0, server.takeRequest().getSequenceNumber());
}
void assertContent(String expected, HttpURLConnection connection, int limit)
@@ -432,14 +423,6 @@
assertEquals(expected, readAscii(connection.getInputStream(), limit));
}
- private void assertContainsNoneMatching(List<String> headers, String pattern) {
- for (String header : headers) {
- if (header.matches(pattern)) {
- fail("Header " + header + " matches " + pattern);
- }
- }
- }
-
private String readAscii(InputStream in, int count) throws IOException {
StringBuilder result = new StringBuilder();
for (int i = 0; i < count; i++) {
@@ -453,12 +436,12 @@
return result.toString();
}
- public byte[] gzip(byte[] bytes) throws IOException {
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
- gzippedOut.write(bytes);
- gzippedOut.close();
- return bytesOut.toByteArray();
+ public Buffer gzip(String bytes) throws IOException {
+ Buffer bytesOut = new Buffer();
+ BufferedSink sink = Okio.buffer(new GzipSink(bytesOut));
+ sink.writeUtf8(bytes);
+ sink.close();
+ return bytesOut;
}
class SpdyRequest implements Runnable {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RecordingProxySelector.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RecordingProxySelector.java
new file mode 100644
index 0000000..ed9cfa4
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RecordingProxySelector.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2014 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 java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public final class RecordingProxySelector extends ProxySelector {
+ final List<URI> requestedUris = new ArrayList<>();
+ List<Proxy> proxies = new ArrayList<>();
+ final List<String> failures = new ArrayList<>();
+
+ @Override public List<Proxy> select(URI uri) {
+ requestedUris.add(uri);
+ return proxies;
+ }
+
+ public void assertRequests(URI... expectedUris) {
+ assertEquals(Arrays.asList(expectedUris), requestedUris);
+ requestedUris.clear();
+ }
+
+ @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+ InetSocketAddress socketAddress = (InetSocketAddress) sa;
+ failures.add(
+ String.format("%s %s:%d %s", uri, socketAddress.getHostName(), socketAddress.getPort(),
+ ioe.getMessage()));
+ }
+}
diff --git a/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 e4f5a5a..8efd308 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
@@ -16,20 +16,22 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
-import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.Authenticator;
import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.HostResolver;
-import com.squareup.okhttp.OkAuthenticator;
+import com.squareup.okhttp.ConnectionSpec;
+import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.RouteDatabase;
+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;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
-import java.net.ProxySelector;
-import java.net.SocketAddress;
-import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -37,8 +39,11 @@
import java.util.NoSuchElementException;
import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
+import org.junit.Before;
import org.junit.Test;
import static java.net.Proxy.NO_PROXY;
@@ -48,6 +53,11 @@
import static org.junit.Assert.fail;
public final class RouteSelectorTest {
+ public final List<ConnectionSpec> connectionSpecs = Util.immutableList(
+ ConnectionSpec.MODERN_TLS,
+ ConnectionSpec.COMPATIBLE_TLS,
+ ConnectionSpec.CLEARTEXT);
+
private static final int proxyAPort = 1001;
private static final String proxyAHost = "proxyA";
private static final Proxy proxyA =
@@ -56,82 +66,97 @@
private static final String proxyBHost = "proxyB";
private static final Proxy proxyB =
new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort));
- private static final URI uri;
- private static final String uriHost = "hostA";
- private static final int uriPort = 80;
+ private String uriHost = "hostA";
+ private int uriPort = 1003;
- private static final SocketFactory socketFactory;
- private static final SSLContext sslContext = SslContextBuilder.localhost();
- private static final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
- private static final HostnameVerifier hostnameVerifier;
- private static final ConnectionPool pool;
+ private SocketFactory socketFactory;
+ private final SSLContext sslContext = SslContextBuilder.localhost();
+ private final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
+ private HostnameVerifier hostnameVerifier;
- static {
- try {
- uri = new URI("http://" + uriHost + ":" + uriPort + "/path");
- socketFactory = SocketFactory.getDefault();
- pool = ConnectionPool.getDefault();
- hostnameVerifier = HttpsURLConnectionImpl.getDefaultHostnameVerifier();
- } catch (Exception e) {
- throw new AssertionError(e);
- }
+ private final Authenticator authenticator = AuthenticatorAdapter.INSTANCE;
+ 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;
+
+ @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();
}
- private final OkAuthenticator authenticator = HttpAuthenticator.SYSTEM_DEFAULT;
- private final List<Protocol> protocols = Arrays.asList(Protocol.HTTP_11);
- private final FakeDns dns = new FakeDns();
- private final FakeProxySelector proxySelector = new FakeProxySelector();
-
@Test public void singleRoute() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
+ Address address = httpAddress();
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.CLEARTEXT);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
try {
- routeSelector.next("GET");
+ routeSelector.next();
fail();
} catch (NoSuchElementException expected) {
}
}
@Test public void singleRouteReturnsFailedRoute() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
+ Address address = httpAddress();
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- Connection connection = routeSelector.next("GET");
- RouteDatabase routeDatabase = new RouteDatabase();
- routeDatabase.failed(connection.getRoute());
- routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ Route route = routeSelector.next();
+ routeDatabase.failed(route);
+ routeSelector = RouteSelector.get(address, httpRequest, client);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.CLEARTEXT);
assertFalse(routeSelector.hasNext());
try {
- routeSelector.next("GET");
+ routeSelector.next();
fail();
} catch (NoSuchElementException expected) {
}
}
- @Test public void explicitProxyTriesThatProxiesAddressesOnly() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator,
- proxyA, protocols);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
+ @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);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0],
+ proxyAPort, ConnectionSpec.CLEARTEXT);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1],
+ proxyAPort, ConnectionSpec.CLEARTEXT);
assertFalse(routeSelector.hasNext());
dns.assertRequests(proxyAHost);
@@ -139,124 +164,126 @@
}
@Test public void explicitDirectProxy() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator,
- NO_PROXY, protocols);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
+ 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);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.CLEARTEXT);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1],
+ uriPort, ConnectionSpec.CLEARTEXT);
assertFalse(routeSelector.hasNext());
- dns.assertRequests(uri.getHost());
+ dns.assertRequests(uriHost);
proxySelector.assertRequests(); // No proxy selector requests!
}
@Test public void proxySelectorReturnsNull() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
+ Address address = httpAddress();
proxySelector.proxies = null;
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
- proxySelector.assertRequests(uri);
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ proxySelector.assertRequests(httpRequest.uri());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.CLEARTEXT);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
}
@Test public void proxySelectorReturnsNoProxies() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
+ Address address = httpAddress();
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.CLEARTEXT);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1],
+ uriPort, ConnectionSpec.CLEARTEXT);
assertFalse(routeSelector.hasNext());
- dns.assertRequests(uri.getHost());
- proxySelector.assertRequests(uri);
+ dns.assertRequests(uriHost);
+ proxySelector.assertRequests(httpRequest.uri());
}
@Test public void proxySelectorReturnsMultipleProxies() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
+ Address address = httpAddress();
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
- proxySelector.assertRequests(uri);
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ proxySelector.assertRequests(httpRequest.uri());
// First try the IP addresses of the first proxy, in sequence.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort,
+ ConnectionSpec.CLEARTEXT);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort,
+ ConnectionSpec.CLEARTEXT);
dns.assertRequests(proxyAHost);
// Next try the IP address of the second proxy.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(254, 1);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort);
+ assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0],
+ proxyBPort,
+ ConnectionSpec.CLEARTEXT);
dns.assertRequests(proxyBHost);
// Finally try the only IP address of the origin server.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(253, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort,
+ ConnectionSpec.CLEARTEXT);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
}
@Test public void proxySelectorDirectConnectionsAreSkipped() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
+ Address address = httpAddress();
proxySelector.proxies.add(NO_PROXY);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
- proxySelector.assertRequests(uri);
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ proxySelector.assertRequests(httpRequest.uri());
// Only the origin server will be attempted.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort,
+ ConnectionSpec.CLEARTEXT);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
}
@Test public void proxyDnsFailureContinuesToNextProxy() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, authenticator, null,
- protocols);
+ Address address = httpAddress();
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
proxySelector.proxies.add(proxyA);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
- proxySelector.assertRequests(uri);
+ RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ proxySelector.assertRequests(httpRequest.uri());
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0],
+ proxyAPort, ConnectionSpec.CLEARTEXT);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = null;
try {
- routeSelector.next("GET");
+ routeSelector.next();
fail();
} catch (UnknownHostException expected) {
}
@@ -264,75 +291,116 @@
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0],
+ proxyAPort, ConnectionSpec.CLEARTEXT);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(254, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.CLEARTEXT);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
}
- @Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
- hostnameVerifier, authenticator, null, protocols);
+ // https://github.com/square/okhttp/issues/442
+ @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception {
+ Address address = httpsAddress();
+ client.setProxy(Proxy.NO_PROXY);
+ RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+
+ dns.inetAddresses = makeFakeAddresses(255, 1);
+ Route route = routeSelector.next();
+ routeSelector.connectFailed(route, new IOException("Non SSL exception"));
+ assertEquals(2, routeDatabase.failedRoutesCount());
+ assertFalse(routeSelector.hasNext());
+ }
+
+ @Test public void sslErrorAddsOnlyFailedConfigurationToFailedRoute() throws Exception {
+ Address address = httpsAddress();
+ client.setProxy(Proxy.NO_PROXY);
+ RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+
+ dns.inetAddresses = makeFakeAddresses(255, 1);
+ Route route = routeSelector.next();
+ routeSelector.connectFailed(route, new SSLHandshakeException("SSL exception"));
+ assertTrue(routeDatabase.failedRoutesCount() == 1);
+ assertTrue(routeSelector.hasNext());
+ }
+
+ @Test public void multipleProxiesMultipleInetAddressesMultipleConfigurations() throws Exception {
+ Address address = httpsAddress();
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- new RouteDatabase());
+ RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
// Proxy A
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0],
+ proxyAPort, ConnectionSpec.MODERN_TLS);
dns.assertRequests(proxyAHost);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0],
+ proxyAPort, ConnectionSpec.COMPATIBLE_TLS);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1],
+ proxyAPort, ConnectionSpec.MODERN_TLS);
+ assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1],
+ proxyAPort, ConnectionSpec.COMPATIBLE_TLS);
// Proxy B
dns.inetAddresses = makeFakeAddresses(254, 2);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort);
+ assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0],
+ proxyBPort, ConnectionSpec.MODERN_TLS);
dns.assertRequests(proxyBHost);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[1], proxyBPort);
+ assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0],
+ proxyBPort, ConnectionSpec.COMPATIBLE_TLS);
+ assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[1],
+ proxyBPort, ConnectionSpec.MODERN_TLS);
+ assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[1],
+ proxyBPort, ConnectionSpec.COMPATIBLE_TLS);
// Origin
dns.inetAddresses = makeFakeAddresses(253, 2);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.MODERN_TLS);
dns.assertRequests(uriHost);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0],
+ uriPort, ConnectionSpec.COMPATIBLE_TLS);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1],
+ uriPort, ConnectionSpec.MODERN_TLS);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1],
+ uriPort, ConnectionSpec.COMPATIBLE_TLS);
assertFalse(routeSelector.hasNext());
}
@Test public void failedRoutesAreLast() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
- hostnameVerifier, authenticator, Proxy.NO_PROXY, protocols);
+ Address address = httpsAddress();
+ client.setProxy(Proxy.NO_PROXY);
+ RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
- RouteDatabase routeDatabase = new RouteDatabase();
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- routeDatabase);
- dns.inetAddresses = makeFakeAddresses(255, 2);
+ dns.inetAddresses = makeFakeAddresses(255, 1);
// Extract the regular sequence of routes from selector.
- List<Connection> regularRoutes = new ArrayList<Connection>();
+ List<Route> regularRoutes = new ArrayList<>();
while (routeSelector.hasNext()) {
- regularRoutes.add(routeSelector.next("GET"));
+ regularRoutes.add(routeSelector.next());
}
// Check that we do indeed have more than one route.
assertTrue(regularRoutes.size() > 1);
// Add first regular route as failed.
- routeDatabase.failed(regularRoutes.get(0).getRoute());
+ routeDatabase.failed(regularRoutes.get(0));
// Reset selector
- routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
+ routeSelector = RouteSelector.get(address, httpsRequest, client);
- List<Connection> routesWithFailedRoute = new ArrayList<Connection>();
+ List<Route> routesWithFailedRoute = new ArrayList<>();
while (routeSelector.hasNext()) {
- routesWithFailedRoute.add(routeSelector.next("GET"));
+ routesWithFailedRoute.add(routeSelector.next());
}
- assertEquals(regularRoutes.get(0).getRoute(),
- routesWithFailedRoute.get(routesWithFailedRoute.size() - 1).getRoute());
+ assertEquals(regularRoutes.get(0),
+ routesWithFailedRoute.get(routesWithFailedRoute.size() - 1));
assertEquals(regularRoutes.size(), routesWithFailedRoute.size());
}
@@ -354,12 +422,24 @@
assertEquals("127.0.0.1", RouteSelector.getHostString(socketAddress));
}
- private void assertConnection(Connection connection, Address address, Proxy proxy,
- InetAddress socketAddress, int socketPort) {
- assertEquals(address, connection.getRoute().getAddress());
- assertEquals(proxy, connection.getRoute().getProxy());
- assertEquals(socketAddress, connection.getRoute().getSocketAddress().getAddress());
- assertEquals(socketPort, connection.getRoute().getSocketAddress().getPort());
+ private void assertRoute(Route route, Address address, Proxy proxy,
+ InetAddress socketAddress, int socketPort, ConnectionSpec connectionSpec) {
+ assertEquals(address, route.getAddress());
+ assertEquals(proxy, route.getProxy());
+ assertEquals(socketAddress, route.getSocketAddress().getAddress());
+ assertEquals(socketPort, route.getSocketAddress().getPort());
+ assertEquals(connectionSpec, route.getConnectionSpec());
+ }
+
+ /** 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,
+ protocols, connectionSpecs, proxySelector);
+ }
+
+ private Address httpsAddress() {
+ return new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+ hostnameVerifier, null, authenticator, null, protocols, connectionSpecs, proxySelector);
}
private static InetAddress[] makeFakeAddresses(int prefix, int count) {
@@ -375,11 +455,11 @@
}
}
- private static class FakeDns implements HostResolver {
- List<String> requestedHosts = new ArrayList<String>();
+ private static class FakeDns implements Network {
+ List<String> requestedHosts = new ArrayList<>();
InetAddress[] inetAddresses;
- @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
+ @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
requestedHosts.add(host);
if (inetAddresses == null) throw new UnknownHostException();
return inetAddresses;
@@ -390,27 +470,4 @@
requestedHosts.clear();
}
}
-
- private static class FakeProxySelector extends ProxySelector {
- List<URI> requestedUris = new ArrayList<URI>();
- List<Proxy> proxies = new ArrayList<Proxy>();
- List<String> failures = new ArrayList<String>();
-
- @Override public List<Proxy> select(URI uri) {
- requestedUris.add(uri);
- return proxies;
- }
-
- public void assertRequests(URI... expectedUris) {
- assertEquals(Arrays.asList(expectedUris), requestedUris);
- requestedUris.clear();
- }
-
- @Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
- InetSocketAddress socketAddress = (InetSocketAddress) sa;
- failures.add(
- String.format("%s %s:%d %s", uri, socketAddress.getHostName(), socketAddress.getPort(),
- ioe.getMessage()));
- }
- }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java
index 885570a..f339f9e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/StatusLineTest.java
@@ -15,6 +15,7 @@
*/
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.Protocol;
import java.io.IOException;
import java.net.ProtocolException;
import org.junit.Test;
@@ -27,19 +28,19 @@
String message = "Temporary Redirect";
int version = 1;
int code = 200;
- StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code + " " + message);
- assertEquals(message, statusLine.message());
- assertEquals(version, statusLine.httpMinorVersion());
- assertEquals(code, statusLine.code());
+ StatusLine statusLine = StatusLine.parse("HTTP/1." + version + " " + code + " " + message);
+ assertEquals(message, statusLine.message);
+ assertEquals(Protocol.HTTP_1_1, statusLine.protocol);
+ assertEquals(code, statusLine.code);
}
@Test public void emptyMessage() throws IOException {
int version = 1;
int code = 503;
- StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code + " ");
- assertEquals("", statusLine.message());
- assertEquals(version, statusLine.httpMinorVersion());
- assertEquals(code, statusLine.code());
+ StatusLine statusLine = StatusLine.parse("HTTP/1." + version + " " + code + " ");
+ assertEquals("", statusLine.message);
+ assertEquals(Protocol.HTTP_1_1, statusLine.protocol);
+ assertEquals(code, statusLine.code);
}
/**
@@ -50,18 +51,18 @@
@Test public void emptyMessageAndNoLeadingSpace() throws IOException {
int version = 1;
int code = 503;
- StatusLine statusLine = new StatusLine("HTTP/1." + version + " " + code);
- assertEquals("", statusLine.message());
- assertEquals(version, statusLine.httpMinorVersion());
- assertEquals(code, statusLine.code());
+ StatusLine statusLine = StatusLine.parse("HTTP/1." + version + " " + code);
+ assertEquals("", statusLine.message);
+ assertEquals(Protocol.HTTP_1_1, statusLine.protocol);
+ assertEquals(code, statusLine.code);
}
// https://github.com/square/okhttp/issues/386
@Test public void shoutcast() throws IOException {
- StatusLine statusLine = new StatusLine("ICY 200 OK");
- assertEquals("OK", statusLine.message());
- assertEquals(0, statusLine.httpMinorVersion());
- assertEquals(200, statusLine.code());
+ StatusLine statusLine = StatusLine.parse("ICY 200 OK");
+ assertEquals("OK", statusLine.message);
+ assertEquals(Protocol.HTTP_1_0, statusLine.protocol);
+ assertEquals(200, statusLine.code);
}
@Test public void missingProtocol() throws IOException {
@@ -109,7 +110,7 @@
private void assertInvalid(String statusLine) throws IOException {
try {
- new StatusLine(statusLine);
+ StatusLine.parse(statusLine);
fail();
} catch (ProtocolException expected) {
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
index 7e7ce0b..63f55e1 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/ThreadInterruptTest.java
@@ -15,33 +15,71 @@
*/
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.DelegatingServerSocketFactory;
+import com.squareup.okhttp.DelegatingSocketFactory;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
+
+import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
+import java.net.ServerSocket;
+import java.net.Socket;
import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import org.junit.Before;
import org.junit.Test;
+import javax.net.ServerSocketFactory;
+import javax.net.SocketFactory;
+
import static org.junit.Assert.fail;
public final class ThreadInterruptTest {
- private final MockWebServer server = new MockWebServer();
- private final OkHttpClient client = new OkHttpClient();
+
+ // The size of the socket buffers in bytes.
+ private static final int SOCKET_BUFFER_SIZE = 256 * 1024;
+
+ private MockWebServer server;
+ private OkHttpClient client;
+
+ @Before public void setUp() throws Exception {
+ server = new MockWebServer();
+ client = new OkHttpClient();
+
+ // Sockets on some platforms can have large buffers that mean writes do not block when
+ // required. These socket factories explicitly set the buffer sizes on sockets created.
+ server.setServerSocketFactory(
+ new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
+ @Override
+ protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+ serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ }
+ });
+ client.setSocketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
+ @Override
+ protected void configureSocket(Socket socket) throws IOException {
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ }
+ });
+ }
@Test public void interruptWritingRequestBody() throws Exception {
- int requestBodySize = 10 * 1024 * 1024; // 10 MiB
+ int requestBodySize = 2 * 1024 * 1024; // 2 MiB
server.enqueue(new MockResponse()
.throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
- server.play();
+ server.start();
interruptLater(500);
- HttpURLConnection connection = client.open(server.getUrl("/"));
+ HttpURLConnection connection = new OkUrlFactory(client).open(server.getUrl("/"));
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(requestBodySize);
OutputStream requestBody = connection.getOutputStream();
@@ -59,16 +97,16 @@
}
@Test public void interruptReadingResponseBody() throws Exception {
- int responseBodySize = 10 * 1024 * 1024; // 10 MiB
+ int responseBodySize = 2 * 1024 * 1024; // 2 MiB
server.enqueue(new MockResponse()
- .setBody(new byte[responseBodySize])
+ .setBody(new Buffer().write(new byte[responseBodySize]))
.throttleBody(64 * 1024, 125, TimeUnit.MILLISECONDS)); // 500 Kbps
- server.play();
+ server.start();
interruptLater(500);
- HttpURLConnection connection = client.open(server.getUrl("/"));
+ HttpURLConnection connection = new OkUrlFactory(client).open(server.getUrl("/"));
InputStream responseBody = connection.getInputStream();
byte[] buffer = new byte[1024];
try {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 4d0a00e..330929b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -16,30 +16,37 @@
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.Cache;
+import com.squareup.okhttp.Challenge;
import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.HttpResponseCache;
-import com.squareup.okhttp.LimitedProtocolsSocketFactory;
-import com.squareup.okhttp.OkAuthenticator.Challenge;
-import com.squareup.okhttp.OkAuthenticator.Credential;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.ConnectionSpec;
+import com.squareup.okhttp.Credentials;
+import com.squareup.okhttp.DelegatingServerSocketFactory;
+import com.squareup.okhttp.DelegatingSocketFactory;
import com.squareup.okhttp.FallbackTestClientSocketFactory;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.TlsVersion;
+import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.RecordingAuthenticator;
import com.squareup.okhttp.internal.RecordingHostnameVerifier;
import com.squareup.okhttp.internal.RecordingOkAuthenticator;
+import com.squareup.okhttp.internal.SingleInetAddressNetwork;
import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
import com.squareup.okhttp.mockwebserver.SocketPolicy;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Authenticator;
-import java.net.CacheRequest;
-import java.net.CacheResponse;
import java.net.ConnectException;
import java.net.HttpRetryException;
import java.net.HttpURLConnection;
@@ -47,10 +54,9 @@
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.ProxySelector;
-import java.net.ResponseCache;
+import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
-import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
@@ -60,33 +66,37 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashSet;
-import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
-import java.util.UUID;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.GZIPInputStream;
-import java.util.zip.GZIPOutputStream;
+import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.GzipSink;
+import okio.Okio;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
import static com.squareup.okhttp.internal.Util.UTF_8;
import static com.squareup.okhttp.internal.http.OkHeaders.SELECTED_PROTOCOL;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
@@ -105,29 +115,28 @@
public final class URLConnectionTest {
private static final SSLContext sslContext = SslContextBuilder.localhost();
- private MockWebServer server = new MockWebServer();
- private MockWebServer server2 = new MockWebServer();
+ @Rule public final MockWebServerRule server = new MockWebServerRule();
+ @Rule public final MockWebServerRule server2 = new MockWebServerRule();
+ @Rule public final TemporaryFolder tempDir = new TemporaryFolder();
- private final OkHttpClient client = new OkHttpClient();
+ private OkUrlFactory client;
private HttpURLConnection connection;
- private HttpResponseCache cache;
- private String hostName;
+ private Cache cache;
@Before public void setUp() throws Exception {
- hostName = server.getHostName();
- server.setNpnEnabled(false);
+ server.get().setProtocolNegotiationEnabled(false);
+ client = new OkUrlFactory(new OkHttpClient());
}
@After public void tearDown() throws Exception {
Authenticator.setDefault(null);
System.clearProperty("proxyHost");
System.clearProperty("proxyPort");
+ System.clearProperty("http.agent");
System.clearProperty("http.proxyHost");
System.clearProperty("http.proxyPort");
System.clearProperty("https.proxyHost");
System.clearProperty("https.proxyPort");
- server.shutdown();
- server2.shutdown();
if (cache != null) {
cache.delete();
}
@@ -135,7 +144,6 @@
@Test public void requestHeaders() throws IOException, InterruptedException {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
connection.addRequestProperty("D", "e");
@@ -172,12 +180,11 @@
connection.getResponseCode();
RecordedRequest request = server.takeRequest();
- assertContains(request.getHeaders(), "D: e");
- assertContains(request.getHeaders(), "D: f");
- assertContainsNoneMatching(request.getHeaders(), "NullValue.*");
- assertContainsNoneMatching(request.getHeaders(), "AnotherNullValue.*");
- assertContainsNoneMatching(request.getHeaders(), "G:.*");
- assertContainsNoneMatching(request.getHeaders(), "null:.*");
+ assertEquals(Arrays.asList("e", "f"), request.getHeaders().values("D"));
+ assertNull(request.getHeader("NullValue"));
+ assertNull(request.getHeader("AnotherNullValue"));
+ assertNull(request.getHeader("G"));
+ assertNull(request.getHeader("null"));
try {
connection.addRequestProperty("N", "o");
@@ -197,7 +204,6 @@
}
@Test public void getRequestPropertyReturnsLastValue() throws Exception {
- server.play();
connection = client.open(server.getUrl("/"));
connection.addRequestProperty("A", "value1");
connection.addRequestProperty("A", "value2");
@@ -210,7 +216,6 @@
.addHeader("B: d")
.addHeader("A: e")
.setChunkedBody("ABCDE\nFGHIJ\nKLMNO\nPQR", 8));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals(200, connection.getResponseCode());
@@ -240,7 +245,6 @@
@Test public void serverSendsInvalidResponseHeaders() throws Exception {
server.enqueue(new MockResponse().setStatus("HTP/1.1 200 OK"));
- server.play();
connection = client.open(server.getUrl("/"));
try {
@@ -252,7 +256,6 @@
@Test public void serverSendsInvalidCodeTooLarge() throws Exception {
server.enqueue(new MockResponse().setStatus("HTTP/1.1 2147483648 OK"));
- server.play();
connection = client.open(server.getUrl("/"));
try {
@@ -264,7 +267,6 @@
@Test public void serverSendsInvalidCodeNotANumber() throws Exception {
server.enqueue(new MockResponse().setStatus("HTTP/1.1 00a OK"));
- server.play();
connection = client.open(server.getUrl("/"));
try {
@@ -276,7 +278,6 @@
@Test public void serverSendsUnnecessaryWhitespace() throws Exception {
server.enqueue(new MockResponse().setStatus(" HTTP/1.1 2147483648 OK"));
- server.play();
connection = client.open(server.getUrl("/"));
try {
@@ -287,9 +288,8 @@
}
@Test public void connectRetriesUntilConnectedOrFailed() throws Exception {
- server.play();
URL url = server.getUrl("/foo");
- server.shutdown();
+ server.get().shutdown();
connection = client.open(url);
try {
@@ -313,14 +313,12 @@
private void testRequestBodySurvivesRetries(TransferKind transferKind) throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
- server.play();
// Use a misconfigured proxy to guarantee that the request is retried.
- server2.play();
FakeProxySelector proxySelector = new FakeProxySelector();
- proxySelector.proxies.add(server2.toProxyAddress());
- client.setProxySelector(proxySelector);
- server2.shutdown();
+ proxySelector.proxies.add(server2.get().toProxyAddress());
+ client.client().setProxySelector(proxySelector);
+ server2.get().shutdown();
connection = client.open(server.getUrl("/def"));
connection.setDoOutput(true);
@@ -328,19 +326,17 @@
connection.getOutputStream().write("body".getBytes("UTF-8"));
assertContent("abc", connection);
- assertEquals("body", server.takeRequest().getUtf8Body());
+ assertEquals("body", server.takeRequest().getBody().readUtf8());
}
@Test public void getErrorStreamOnSuccessfulRequest() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
assertNull(connection.getErrorStream());
}
@Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
server.enqueue(new MockResponse().setResponseCode(404).setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection.getErrorStream(), Integer.MAX_VALUE));
}
@@ -353,7 +349,6 @@
server.enqueue(response);
server.enqueue(response);
- server.play();
assertContent("ABCDE", client.open(server.getUrl("/")), 5);
assertContent("ABCDE", client.open(server.getUrl("/")), 5);
@@ -372,7 +367,6 @@
server.enqueue(response);
server.enqueue(response);
server.enqueue(response);
- server.play();
assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo")));
assertEquals(0, server.takeRequest().getSequenceNumber());
@@ -388,7 +382,6 @@
server.enqueue(response);
server.enqueue(response);
server.enqueue(response);
- server.play();
assertContent("ABCDEFGHIJKLMNOPQR", client.open(server.getUrl("/foo")));
assertEquals(0, server.takeRequest().getSequenceNumber());
@@ -427,7 +420,6 @@
MockResponse responseAfter = new MockResponse().setBody("This comes after a busted connection");
server.enqueue(responseAfter);
server.enqueue(responseAfter); // Enqueue 2x because the broken connection may be reused.
- server.play();
HttpURLConnection connection1 = client.open(server.getUrl("/a"));
connection1.setReadTimeout(100);
@@ -474,9 +466,8 @@
private void doUpload(TransferKind uploadKind, WriteKind writeKind) throws Exception {
int n = 512 * 1024;
- server.setBodyLimit(0);
+ server.get().setBodyLimit(0);
server.enqueue(new MockResponse());
- server.play();
HttpURLConnection conn = client.open(server.getUrl("/"));
conn.setDoOutput(true);
@@ -511,7 +502,6 @@
@Test public void getResponseCodeNoResponseBody() throws Exception {
server.enqueue(new MockResponse().addHeader("abc: def"));
- server.play();
URL url = server.getUrl("/");
HttpURLConnection conn = client.open(url);
@@ -526,12 +516,11 @@
}
@Test public void connectViaHttps() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/foo"));
assertContent("this response comes via HTTPS", connection);
@@ -541,12 +530,11 @@
}
@Test public void inspectHandshakeThroughoutRequestLifecycle() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse());
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
HttpsURLConnection httpsConnection = (HttpsURLConnection) client.open(server.getUrl("/foo"));
@@ -571,17 +559,16 @@
}
@Test public void connectViaHttpsReusingConnections() throws IOException, InterruptedException {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.enqueue(new MockResponse().setBody("another response via HTTPS"));
- server.play();
// The pool will only reuse sockets if the SSL socket factories are the same.
SSLSocketFactory clientSocketFactory = sslContext.getSocketFactory();
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(hostnameVerifier);
+ client.client().setSslSocketFactory(clientSocketFactory);
+ client.client().setHostnameVerifier(hostnameVerifier);
connection = client.open(server.getUrl("/"));
assertContent("this response comes via HTTPS", connection);
@@ -594,18 +581,17 @@
@Test public void connectViaHttpsReusingConnectionsDifferentFactories()
throws IOException, InterruptedException {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
server.enqueue(new MockResponse().setBody("another response via HTTPS"));
- server.play();
// install a custom SSL socket factory so the server can be authorized
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
HttpURLConnection connection1 = client.open(server.getUrl("/"));
assertContent("this response comes via HTTPS", connection1);
- client.setSslSocketFactory(null);
+ client.client().setSslSocketFactory(null);
HttpURLConnection connection2 = client.open(server.getUrl("/"));
try {
readAscii(connection2.getInputStream(), Integer.MAX_VALUE);
@@ -614,132 +600,19 @@
}
}
- @Test public void connectViaHttpsWithSSLFallback() throws Exception {
- SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
-
- server.useHttps(socketFactory, false);
+ @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setBody("this response comes via SSL"));
- server.play();
- final boolean disableTlsFallbackScsv = true;
- FallbackTestClientSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ suppressTlsFallbackScsv(client.client());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/foo"));
assertContent("this response comes via SSL", connection);
- assertEquals(2, server.getRequestCount());
RecordedRequest request = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request.getRequestLine());
- assertEquals("SSLv3", request.getSslProtocol());
- assertEquals(2, clientSocketFactory.getCreatedSockets().size());
- }
-
- @Test public void connectViaHttpsWithSSLFallback_serverDoesNotSupportFallbackProtocol()
- throws Exception {
- SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1");
- server.useHttps(serverSocketFactory, false);
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
- server.play();
-
- final boolean disableTlsFallbackScsv = true;
- FallbackTestClientSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(
- new LimitedProtocolsSocketFactory(sslContext.getSocketFactory(), "TLSv1", "SSLv3"),
- disableTlsFallbackScsv);
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- connection = client.open(server.getUrl("/foo"));
-
- try {
- connection = client.open(server.getUrl("/foo"));
- connection.getInputStream();
- fail();
- } catch (SSLHandshakeException expected) {
- }
-
- // The first request is handled by MockWebServer and intentionally failed.
- assertEquals(1, server.getRequestCount());
- // The client will attempt a fallback connection using SSLv3, but fail because the server does
- // not support it.
- assertEquals(2, clientSocketFactory.getCreatedSockets().size());
- }
-
- @Test public void connectViaHttpsWithSSLFallback_clientDoesNotSupportFallbackProtocol()
- throws Exception {
-
- SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
- server.useHttps(serverSocketFactory, false);
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
- server.play();
-
- final boolean disableTlsFallbackScsv = true;
- FallbackTestClientSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(
- new LimitedProtocolsSocketFactory(sslContext.getSocketFactory(), "TLSv1"),
- disableTlsFallbackScsv);
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- connection = client.open(server.getUrl("/foo"));
-
- try {
- connection = client.open(server.getUrl("/foo"));
- connection.getInputStream();
- fail();
- } catch (SSLHandshakeException expected) {
- }
-
- // The first request is handled by MockWebServer and intentionally failed.
- assertEquals(1, server.getRequestCount());
- // The client will not attempt a fallback connection if there is no support for the fallback
- // protocol on the client.
- assertEquals(1, clientSocketFactory.getCreatedSockets().size());
- }
-
- // After the introduction of the TLS_FALLBACK_SCSV we expect a failure if the initial
- // handshake fails and the server supports TLS_FALLBACK_SCSV. MockWebServer on Android uses
- // sockets that enforced TLS_FALLBACK_SCSV checks by default.
- @Test public void connectViaHttpsWithSSLFallback_scsvFailure() throws Exception {
- SSLSocketFactory serverSocketFactory = sslContext.getSocketFactory();
- SSLSocket socket = (SSLSocket) serverSocketFactory.createSocket();
- boolean tlsFallbackScsvSupported = Arrays.asList(socket.getSupportedCipherSuites())
- .contains(FallbackTestClientSocketFactory.TLS_FALLBACK_SCSV);
- socket.close();
- if (!tlsFallbackScsvSupported) {
- // SCSV not supported on this platform. Skip this test.
- return;
- }
-
- SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
-
- server.useHttps(socketFactory, false);
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
- server.play();
-
- final boolean disableTlsFallbackScsv = false;
- FallbackTestClientSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
- client.setSslSocketFactory(clientSocketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- try {
- connection = client.open(server.getUrl("/foo"));
- connection.getInputStream();
- fail();
- } catch (SSLHandshakeException expected) {
- }
-
- // The first request is handled by MockWebServer and intentionally failed.
- assertEquals(1, server.getRequestCount());
- // There will be one fallback attempt with the enabled client protocols. Though supported by the
- // server it will fail because of the TLS_FALLBACK_SCSV check.
- assertEquals(2, clientSocketFactory.getCreatedSockets().size());
}
/**
@@ -749,26 +622,26 @@
* https://github.com/square/okhttp/issues/515
*/
@Test public void sslFallbackNotUsedWhenRecycledConnectionFails() throws Exception {
- SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
- server.useHttps(socketFactory, false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse()
.setBody("abc")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
server.enqueue(new MockResponse().setBody("def"));
- server.play();
- client.setSslSocketFactory(socketFactory);
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ suppressTlsFallbackScsv(client.client());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
assertContent("abc", client.open(server.getUrl("/")));
assertContent("def", client.open(server.getUrl("/")));
+ Set<TlsVersion> tlsVersions =
+ EnumSet.of(TlsVersion.TLS_1_0, TlsVersion.TLS_1_2); // v1.2 on OpenJDK 8.
+
RecordedRequest request1 = server.takeRequest();
- assertEquals("TLSv1", request1.getSslProtocol()); // OkHttp's current best TLS version.
+ assertTrue(tlsVersions.contains(request1.getTlsVersion()));
RecordedRequest request2 = server.takeRequest();
- assertEquals("TLSv1", request2.getSslProtocol()); // OkHttp's current best TLS version.
+ assertTrue(tlsVersions.contains(request2.getTlsVersion()));
}
/**
@@ -777,9 +650,8 @@
* http://code.google.com/p/android/issues/detail?id=13178
*/
@Test public void connectViaHttpsToUntrustedServer() throws IOException, InterruptedException {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse()); // unused
- server.play();
connection = client.open(server.getUrl("/foo"));
try {
@@ -806,24 +678,21 @@
private void testConnectViaProxy(ProxyConfig proxyConfig) throws Exception {
MockResponse mockResponse = new MockResponse().setBody("this response comes via a proxy");
server.enqueue(mockResponse);
- server.play();
URL url = new URL("http://android.com/foo");
- connection = proxyConfig.connect(server, client, url);
+ connection = proxyConfig.connect(server.get(), client, url);
assertContent("this response comes via a proxy", connection);
assertTrue(connection.usingProxy());
- RecordedRequest request = server.takeRequest();
+ RecordedRequest request = server.get().takeRequest();
assertEquals("GET http://android.com/foo HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(), "Host: android.com");
+ assertEquals("android.com", request.getHeader("Host"));
}
@Test public void contentDisagreesWithContentLengthHeader() throws IOException {
server.enqueue(new MockResponse().setBody("abc\r\nYOU SHOULD NOT SEE THIS")
.clearHeaders()
.addHeader("Content-Length: 3"));
- server.play();
-
assertContent("abc", client.open(server.getUrl("/")));
}
@@ -840,15 +709,14 @@
};
if (useHttps) {
- server.useHttps(sslContext.getSocketFactory(), false);
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
}
server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK"));
- server.play();
- client.setSocketFactory(uselessSocketFactory);
+ client.client().setSocketFactory(uselessSocketFactory);
connection = client.open(server.getUrl("/"));
try {
connection.getResponseCode();
@@ -856,7 +724,7 @@
} catch (IllegalArgumentException expected) {
}
- client.setSocketFactory(SocketFactory.getDefault());
+ client.client().setSocketFactory(SocketFactory.getDefault());
connection = client.open(server.getUrl("/"));
assertEquals(200, connection.getResponseCode());
}
@@ -872,15 +740,13 @@
@Test public void contentDisagreesWithChunkedHeader() throws IOException {
MockResponse mockResponse = new MockResponse();
mockResponse.setChunkedBody("abc", 3);
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- bytesOut.write(mockResponse.getBody());
- bytesOut.write("\r\nYOU SHOULD NOT SEE THIS".getBytes("UTF-8"));
- mockResponse.setBody(bytesOut.toByteArray());
+ Buffer buffer = mockResponse.getBody();
+ buffer.writeUtf8("\r\nYOU SHOULD NOT SEE THIS");
+ mockResponse.setBody(buffer);
mockResponse.clearHeaders();
mockResponse.addHeader("Transfer-encoding: chunked");
server.enqueue(mockResponse);
- server.play();
assertContent("abc", client.open(server.getUrl("/")));
}
@@ -895,14 +761,13 @@
}
private void testConnectViaDirectProxyToHttps(ProxyConfig proxyConfig) throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("this response comes via HTTPS"));
- server.play();
URL url = server.getUrl("/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- connection = proxyConfig.connect(server, client, url);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
+ connection = proxyConfig.connect(server.get(), client, url);
assertContent("this response comes via HTTPS", connection);
@@ -933,73 +798,61 @@
private void testConnectViaHttpProxyToHttps(ProxyConfig proxyConfig) throws Exception {
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- server.useHttps(sslContext.getSocketFactory(), true);
+ server.get().useHttps(sslContext.getSocketFactory(), true);
server.enqueue(
new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
server.enqueue(new MockResponse().setBody("this response comes via a secure proxy"));
- server.play();
URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(hostnameVerifier);
- connection = proxyConfig.connect(server, client, url);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(hostnameVerifier);
+ connection = proxyConfig.connect(server.get(), client, url);
assertContent("this response comes via a secure proxy", connection);
RecordedRequest connect = server.takeRequest();
assertEquals("Connect line failure on proxy", "CONNECT android.com:443 HTTP/1.1",
connect.getRequestLine());
- assertContains(connect.getHeaders(), "Host: android.com");
+ assertEquals("android.com", connect.getHeader("Host"));
RecordedRequest get = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
- assertContains(get.getHeaders(), "Host: android.com");
+ assertEquals("android.com", get.getHeader("Host"));
assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
}
- /** Tolerate bad https proxy response when using HttpResponseCache. http://b/6754912 */
+ /** Tolerate bad https proxy response when using HttpResponseCache. Android bug 6754912. */
@Test public void connectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
initResponseCache();
- server.useHttps(sslContext.getSocketFactory(), true);
- MockResponse response = new MockResponse() // Key to reproducing b/6754912
+ server.get().useHttps(sslContext.getSocketFactory(), true);
+ // The inclusion of a body in the response to a CONNECT is key to reproducing b/6754912.
+ MockResponse badProxyResponse = new MockResponse()
.setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
.setBody("bogus proxy connect response content");
+ server.enqueue(badProxyResponse);
+ server.enqueue(new MockResponse().setBody("response"));
- // Enqueue a pair of responses for every IP address held by localhost, because the
- // route selector will try each in sequence.
- // TODO: use the fake Dns implementation instead of a loop
- for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) {
- server.enqueue(response); // For the first TLS tolerant connection
- server.enqueue(response); // For the backwards-compatible SSLv3 retry
- }
- server.play();
- client.setProxy(server.toProxyAddress());
+ // 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().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setConnectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS));
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setProxy(server.get().toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
connection = client.open(url);
-
- try {
- connection.getResponseCode();
- fail();
- } catch (IOException expected) {
- // Thrown when the connect causes SSLSocket.startHandshake() to throw
- // when it sees the "bogus proxy connect response content"
- // instead of a ServerHello handshake message.
- }
+ assertContent("response", connection);
RecordedRequest connect = server.takeRequest();
- assertEquals("Connect line failure on proxy", "CONNECT android.com:443 HTTP/1.1",
- connect.getRequestLine());
- assertContains(connect.getHeaders(), "Host: android.com");
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
+ assertEquals("android.com", connect.getHeader("Host"));
}
private void initResponseCache() throws IOException {
- String tmp = System.getProperty("java.io.tmpdir");
- File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
- cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
- client.setOkResponseCache(cache);
+ cache = new Cache(tempDir.getRoot(), Integer.MAX_VALUE);
+ client.client().setCache(cache);
}
/** Test which headers are sent unencrypted to the HTTP proxy. */
@@ -1007,16 +860,16 @@
throws IOException, InterruptedException {
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- server.useHttps(sslContext.getSocketFactory(), true);
+ server.get().useHttps(sslContext.getSocketFactory(), true);
server.enqueue(
new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
server.enqueue(new MockResponse().setBody("encrypted response from the origin server"));
- server.play();
- client.setProxy(server.toProxyAddress());
+
+ client.client().setProxy(server.get().toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(hostnameVerifier);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(hostnameVerifier);
connection = client.open(url);
connection.addRequestProperty("Private", "Secret");
connection.addRequestProperty("Proxy-Authorization", "bar");
@@ -1024,61 +877,61 @@
assertContent("encrypted response from the origin server", connection);
RecordedRequest connect = server.takeRequest();
- assertContainsNoneMatching(connect.getHeaders(), "Private.*");
- assertContains(connect.getHeaders(), "Proxy-Authorization: bar");
- assertContains(connect.getHeaders(), "User-Agent: baz");
- assertContains(connect.getHeaders(), "Host: android.com");
- assertContains(connect.getHeaders(), "Proxy-Connection: Keep-Alive");
+ assertNull(connect.getHeader("Private"));
+ assertEquals("bar", connect.getHeader("Proxy-Authorization"));
+ assertEquals("baz", connect.getHeader("User-Agent"));
+ assertEquals("android.com", connect.getHeader("Host"));
+ assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
RecordedRequest get = server.takeRequest();
- assertContains(get.getHeaders(), "Private: Secret");
+ assertEquals("Secret", get.getHeader("Private"));
assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
}
@Test public void proxyAuthenticateOnConnect() throws Exception {
Authenticator.setDefault(new RecordingAuthenticator());
- server.useHttps(sslContext.getSocketFactory(), true);
+ server.get().useHttps(sslContext.getSocketFactory(), true);
server.enqueue(new MockResponse().setResponseCode(407)
.addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
server.enqueue(
new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- client.setProxy(server.toProxyAddress());
+
+ client.client().setProxy(server.get().toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(url);
assertContent("A", connection);
RecordedRequest connect1 = server.takeRequest();
assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
- assertContainsNoneMatching(connect1.getHeaders(), "Proxy\\-Authorization.*");
+ assertNull(connect1.getHeader("Proxy-Authorization"));
RecordedRequest connect2 = server.takeRequest();
assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
- assertContains(connect2.getHeaders(),
- "Proxy-Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+ assertEquals("Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS,
+ connect2.getHeader("Proxy-Authorization"));
RecordedRequest get = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
- assertContainsNoneMatching(get.getHeaders(), "Proxy\\-Authorization.*");
+ assertNull(get.getHeader("Proxy-Authorization"));
}
// Don't disconnect after building a tunnel with CONNECT
// http://code.google.com/p/android/issues/detail?id=37221
@Test public void proxyWithConnectionClose() throws IOException {
- server.useHttps(sslContext.getSocketFactory(), true);
+ server.get().useHttps(sslContext.getSocketFactory(), true);
server.enqueue(
new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
server.enqueue(new MockResponse().setBody("this response comes via a proxy"));
- server.play();
- client.setProxy(server.toProxyAddress());
+
+ client.client().setProxy(server.get().toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(url);
connection.setRequestProperty("Connection", "close");
@@ -1089,17 +942,17 @@
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- server.useHttps(socketFactory, true);
+ server.get().useHttps(socketFactory, true);
server.enqueue(
new MockResponse().setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END).clearHeaders());
server.enqueue(new MockResponse().setBody("response 1"));
server.enqueue(new MockResponse().setBody("response 2"));
- server.play();
- client.setProxy(server.toProxyAddress());
+
+ client.client().setProxy(server.get().toProxyAddress());
URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(socketFactory);
- client.setHostnameVerifier(hostnameVerifier);
+ client.client().setSslSocketFactory(socketFactory);
+ client.client().setHostnameVerifier(hostnameVerifier);
assertContent("response 1", client.open(url));
assertContent("response 2", client.open(url));
}
@@ -1108,7 +961,6 @@
server.enqueue(new MockResponse()
.throttleBody(2, 100, TimeUnit.MILLISECONDS)
.setBody("ABCD"));
- server.play();
connection = client.open(server.getUrl("/"));
InputStream in = connection.getInputStream();
@@ -1127,7 +979,6 @@
@Test public void disconnectBeforeConnect() throws IOException {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
connection.disconnect();
@@ -1175,7 +1026,6 @@
transferKind.setBody(response, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", 1024);
server.enqueue(response);
server.enqueue(response);
- server.play();
InputStream in = client.open(server.getUrl("/")).getInputStream();
assertFalse("This implementation claims to support mark().", in.markSupported());
@@ -1202,7 +1052,6 @@
server.enqueue(response);
server.enqueue(response);
server.enqueue(response);
- server.play();
URL url = server.getUrl("/");
HttpURLConnection conn = client.open(url);
@@ -1217,7 +1066,6 @@
server.enqueue(new MockResponse().setBody("5\r\nABCDE\r\nG\r\nFGHIJKLMNOPQRSTU\r\n0\r\n\r\n")
.clearHeaders()
.addHeader("Transfer-encoding: chunked"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
try {
@@ -1232,7 +1080,6 @@
.clearHeaders()
.addHeader("Transfer-encoding: chunked")
.setSocketPolicy(DISCONNECT_AT_END));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
try {
@@ -1248,9 +1095,9 @@
* imply a bug in the implementation.
*/
@Test public void gzipEncodingEnabledByDefault() throws IOException, InterruptedException {
- server.enqueue(new MockResponse().setBody(gzip("ABCABCABC".getBytes("UTF-8")))
+ server.enqueue(new MockResponse()
+ .setBody(gzip("ABCABCABC"))
.addHeader("Content-Encoding: gzip"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
assertEquals("ABCABCABC", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
@@ -1258,24 +1105,23 @@
assertEquals(-1, connection.getContentLength());
RecordedRequest request = server.takeRequest();
- assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+ assertEquals("gzip", request.getHeader("Accept-Encoding"));
}
@Test public void clientConfiguredGzipContentEncoding() throws Exception {
- byte[] bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ".getBytes("UTF-8"));
+ Buffer bodyBytes = gzip("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
server.enqueue(new MockResponse()
.setBody(bodyBytes)
.addHeader("Content-Encoding: gzip"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Accept-Encoding", "gzip");
InputStream gunzippedIn = new GZIPInputStream(connection.getInputStream());
assertEquals("ABCDEFGHIJKLMNOPQRSTUVWXYZ", readAscii(gunzippedIn, Integer.MAX_VALUE));
- assertEquals(bodyBytes.length, connection.getContentLength());
+ assertEquals(bodyBytes.size(), connection.getContentLength());
RecordedRequest request = server.takeRequest();
- assertContains(request.getHeaders(), "Accept-Encoding: gzip");
+ assertEquals("gzip", request.getHeader("Accept-Encoding"));
}
@Test public void gzipAndConnectionReuseWithFixedLength() throws Exception {
@@ -1296,14 +1142,13 @@
@Test public void clientConfiguredCustomContentEncoding() throws Exception {
server.enqueue(new MockResponse().setBody("ABCDE").addHeader("Content-Encoding: custom"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Accept-Encoding", "custom");
assertEquals("ABCDE", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
- assertContains(request.getHeaders(), "Accept-Encoding: custom");
+ assertEquals("custom", request.getHeader("Accept-Encoding"));
}
/**
@@ -1317,19 +1162,18 @@
if (tls) {
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- server.useHttps(socketFactory, false);
- client.setSslSocketFactory(socketFactory);
- client.setHostnameVerifier(hostnameVerifier);
+ server.get().useHttps(socketFactory, false);
+ client.client().setSslSocketFactory(socketFactory);
+ client.client().setHostnameVerifier(hostnameVerifier);
}
MockResponse responseOne = new MockResponse();
responseOne.addHeader("Content-Encoding: gzip");
- transferKind.setBody(responseOne, gzip("one (gzipped)".getBytes("UTF-8")), 5);
+ transferKind.setBody(responseOne, gzip("one (gzipped)"), 5);
server.enqueue(responseOne);
MockResponse responseTwo = new MockResponse();
transferKind.setBody(responseTwo, "two (identity)", 5);
server.enqueue(responseTwo);
- server.play();
HttpURLConnection connection1 = client.open(server.getUrl("/"));
connection1.addRequestProperty("Accept-Encoding", "gzip");
@@ -1348,8 +1192,7 @@
.setSocketPolicy(SHUTDOWN_INPUT_AT_END));
server.enqueue(new MockResponse()
.addHeader("Content-Encoding: gzip")
- .setBody(gzip("b".getBytes(UTF_8))));
- server.play();
+ .setBody(gzip("b")));
// Seed the pool with a bad connection.
assertContent("a", client.open(server.getUrl("/")));
@@ -1366,15 +1209,14 @@
.setBody("{}")
.clearHeaders()
.setSocketPolicy(DISCONNECT_AT_END));
- server.play();
ConnectionPool pool = ConnectionPool.getDefault();
pool.evictAll();
- client.setConnectionPool(pool);
+ client.client().setConnectionPool(pool);
HttpURLConnection connection = client.open(server.getUrl("/"));
assertContent("{}", connection);
- assertEquals(0, client.getConnectionPool().getConnectionCount());
+ assertEquals(0, client.client().getConnectionPool().getConnectionCount());
}
@Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
@@ -1394,8 +1236,6 @@
transferKind.setBody(response2, "LMNOPQRSTUV", 1024);
server.enqueue(response2);
- server.play();
-
HttpURLConnection connection1 = client.open(server.getUrl("/"));
InputStream in1 = connection1.getInputStream();
assertEquals("ABCDE", readAscii(in1, 5));
@@ -1415,10 +1255,9 @@
@Test public void streamDiscardingIsTimely() throws Exception {
// This response takes at least a full second to serve: 10,000 bytes served 100 bytes at a time.
server.enqueue(new MockResponse()
- .setBody(new byte[10000])
+ .setBody(new Buffer().write(new byte[10000]))
.throttleBody(100, 10, MILLISECONDS));
server.enqueue(new MockResponse().setBody("A"));
- server.play();
long startNanos = System.nanoTime();
URLConnection connection1 = client.open(server.getUrl("/"));
@@ -1441,7 +1280,6 @@
@Test public void setChunkedStreamingMode() throws IOException, InterruptedException {
server.enqueue(new MockResponse());
- server.play();
String body = "ABCDEFGHIJKLMNOPQ";
connection = client.open(server.getUrl("/"));
@@ -1452,7 +1290,7 @@
assertEquals(200, connection.getResponseCode());
RecordedRequest request = server.takeRequest();
- assertEquals(body, new String(request.getBody(), "US-ASCII"));
+ assertEquals(body, request.getBody().readUtf8());
assertEquals(Arrays.asList(body.length()), request.getChunkSizes());
}
@@ -1469,7 +1307,6 @@
.addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
.setBody("Please authenticate.");
server.enqueue(pleaseAuthenticate);
- server.play();
Authenticator.setDefault(new RecordingAuthenticator());
connection = client.open(server.getUrl("/"));
@@ -1491,8 +1328,62 @@
// no authorization header for the request...
RecordedRequest request = server.takeRequest();
- assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
- assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+ assertNull(request.getHeader("Authorization"));
+ assertEquals("ABCD", request.getBody().readUtf8());
+ }
+
+ @Test public void postBodyRetransmittedAfterAuthorizationFail() throws Exception {
+ postBodyRetransmittedAfterAuthorizationFail("abc");
+ }
+
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postBodyRetransmittedAfterAuthorizationFail("abc");
+ }
+
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ 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);
+ postBodyRetransmittedAfterAuthorizationFail("");
+ }
+
+ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ postBodyRetransmittedAfterAuthorizationFail("");
+ }
+
+ private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(401));
+ server.enqueue(new MockResponse());
+
+ String credential = Credentials.basic("jesse", "secret");
+ client.client().setAuthenticator(new RecordingOkAuthenticator(credential));
+
+ connection = client.open(server.getUrl("/"));
+ connection.setDoOutput(true);
+ OutputStream outputStream = connection.getOutputStream();
+ outputStream.write(body.getBytes("UTF-8"));
+ outputStream.close();
+ assertEquals(200, connection.getResponseCode());
+
+ RecordedRequest recordedRequest1 = server.takeRequest();
+ assertEquals("POST", recordedRequest1.getMethod());
+ assertEquals(body, recordedRequest1.getBody().readUtf8());
+ assertNull(recordedRequest1.getHeader("Authorization"));
+
+ RecordedRequest recordedRequest2 = server.takeRequest();
+ assertEquals("POST", recordedRequest2.getMethod());
+ assertEquals(body, recordedRequest2.getBody().readUtf8());
+ assertEquals(credential, recordedRequest2.getHeader("Authorization"));
}
@Test public void nonStandardAuthenticationScheme() throws Exception {
@@ -1522,7 +1413,7 @@
String call = calls.get(0);
assertTrue(call, call.contains("host=" + url.getHost()));
assertTrue(call, call.contains("port=" + url.getPort()));
- assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0]));
+ assertTrue(call, call.contains("site=" + url.getHost()));
assertTrue(call, call.contains("url=" + url));
assertTrue(call, call.contains("type=" + Authenticator.RequestorType.SERVER));
assertTrue(call, call.contains("prompt=Bar"));
@@ -1537,7 +1428,7 @@
String call = calls.get(0);
assertTrue(call, call.contains("host=" + url.getHost()));
assertTrue(call, call.contains("port=" + url.getPort()));
- assertTrue(call, call.contains("site=" + InetAddress.getAllByName(url.getHost())[0]));
+ assertTrue(call, call.contains("site=" + url.getHost()));
assertTrue(call, call.contains("url=http://android.com"));
assertTrue(call, call.contains("type=" + Authenticator.RequestorType.PROXY));
assertTrue(call, call.contains("prompt=Bar"));
@@ -1554,10 +1445,9 @@
.addHeader(authHeader)
.setBody("Please authenticate.");
server.enqueue(pleaseAuthenticate);
- server.play();
if (proxy) {
- client.setProxy(server.toProxyAddress());
+ client.client().setProxy(server.get().toProxyAddress());
connection = client.open(new URL("http://android.com"));
} else {
connection = client.open(server.getUrl("/"));
@@ -1567,7 +1457,6 @@
}
@Test public void setValidRequestMethod() throws Exception {
- server.play();
assertValidRequestMethod("GET");
assertValidRequestMethod("DELETE");
assertValidRequestMethod("HEAD");
@@ -1585,12 +1474,10 @@
}
@Test public void setInvalidRequestMethodLowercase() throws Exception {
- server.play();
assertInvalidRequestMethod("get");
}
@Test public void setInvalidRequestMethodConnect() throws Exception {
- server.play();
assertInvalidRequestMethod("CONNECT");
}
@@ -1622,7 +1509,6 @@
.addHeader("Expires: Mon, 26 Jul 1997 05:00:00 GMT")
.addHeader("icy-metaint:16000")
.setBody("mp3 data"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals(200, connection.getResponseCode());
assertEquals("OK", connection.getResponseMessage());
@@ -1630,7 +1516,6 @@
}
@Test public void cannotSetNegativeFixedLengthStreamingMode() throws Exception {
- server.play();
connection = client.open(server.getUrl("/"));
try {
connection.setFixedLengthStreamingMode(-2);
@@ -1640,14 +1525,12 @@
}
@Test public void canSetNegativeChunkedStreamingMode() throws Exception {
- server.play();
connection = client.open(server.getUrl("/"));
connection.setChunkedStreamingMode(-2);
}
@Test public void cannotSetFixedLengthStreamingModeAfterConnect() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
try {
@@ -1659,7 +1542,6 @@
@Test public void cannotSetChunkedStreamingModeAfterConnect() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
try {
@@ -1670,7 +1552,6 @@
}
@Test public void cannotSetFixedLengthStreamingModeAfterChunkedStreamingMode() throws Exception {
- server.play();
connection = client.open(server.getUrl("/"));
connection.setChunkedStreamingMode(1);
try {
@@ -1681,7 +1562,6 @@
}
@Test public void cannotSetChunkedStreamingModeAfterFixedLengthStreamingMode() throws Exception {
- server.play();
connection = client.open(server.getUrl("/"));
connection.setFixedLengthStreamingMode(1);
try {
@@ -1704,12 +1584,11 @@
* http://code.google.com/p/android/issues/detail?id=12860
*/
private void testSecureStreamingPost(StreamingMode streamingMode) throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("Success!"));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/"));
connection.setDoOutput(true);
byte[] requestBody = { 'A', 'B', 'C', 'D' };
@@ -1730,7 +1609,7 @@
} else if (streamingMode == StreamingMode.CHUNKED) {
assertEquals(Arrays.asList(4), request.getChunkSizes());
}
- assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+ assertEquals("ABCD", request.getBody().readUtf8());
}
enum StreamingMode {
@@ -1747,7 +1626,6 @@
server.enqueue(pleaseAuthenticate);
// ...then succeed the fourth time
server.enqueue(new MockResponse().setBody("Successful auth!"));
- server.play();
Authenticator.setDefault(new RecordingAuthenticator());
connection = client.open(server.getUrl("/"));
@@ -1760,15 +1638,15 @@
// no authorization header for the first request...
RecordedRequest request = server.takeRequest();
- assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+ assertNull(request.getHeader("Authorization"));
// ...but the three requests that follow include an authorization header
for (int i = 0; i < 3; i++) {
request = server.takeRequest();
assertEquals("POST / HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(),
- "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
- assertEquals(Arrays.toString(requestBody), Arrays.toString(request.getBody()));
+ assertEquals("Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS,
+ request.getHeader("Authorization"));
+ assertEquals("ABCD", request.getBody().readUtf8());
}
}
@@ -1782,7 +1660,6 @@
server.enqueue(pleaseAuthenticate);
// ...then succeed the fourth time
server.enqueue(new MockResponse().setBody("Successful auth!"));
- server.play();
Authenticator.setDefault(new RecordingAuthenticator());
connection = client.open(server.getUrl("/"));
@@ -1790,14 +1667,14 @@
// no authorization header for the first request...
RecordedRequest request = server.takeRequest();
- assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+ assertNull(request.getHeader("Authorization"));
// ...but the three requests that follow requests include an authorization header
for (int i = 0; i < 3; i++) {
request = server.takeRequest();
assertEquals("GET / HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(),
- "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+ assertEquals("Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS,
+ request.getHeader("Authorization"));
}
}
@@ -1813,9 +1690,8 @@
// ...then succeed the fourth time
MockResponse successfulResponse = new MockResponse()
.addHeader("Content-Encoding", "gzip")
- .setBody(gzip("Successful auth!".getBytes("UTF-8")));
+ .setBody(gzip("Successful auth!"));
server.enqueue(successfulResponse);
- server.play();
Authenticator.setDefault(new RecordingAuthenticator());
connection = client.open(server.getUrl("/"));
@@ -1823,14 +1699,14 @@
// no authorization header for the first request...
RecordedRequest request = server.takeRequest();
- assertContainsNoneMatching(request.getHeaders(), "Authorization: Basic .*");
+ assertNull(request.getHeader("Authorization"));
// ...but the three requests that follow requests include an authorization header
for (int i = 0; i < 3; i++) {
request = server.takeRequest();
assertEquals("GET / HTTP/1.1", request.getRequestLine());
- assertContains(request.getHeaders(),
- "Authorization: Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS);
+ assertEquals("Basic " + RecordingAuthenticator.BASE_64_CREDENTIALS,
+ request.getHeader("Authorization"));
}
}
@@ -1840,7 +1716,6 @@
.addHeader("wWw-aUtHeNtIcAtE: bAsIc rEaLm=\"pRoTeCtEd aReA\"")
.setBody("Please authenticate."));
server.enqueue(new MockResponse().setBody("Successful auth!"));
- server.play();
Authenticator.setDefault(new RecordingAuthenticator());
connection = client.open(server.getUrl("/"));
@@ -1865,7 +1740,6 @@
transferKind.setBody(response, "This page has moved!", 10);
server.enqueue(response);
server.enqueue(new MockResponse().setBody("This is the new location!"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
assertEquals("This is the new location!",
@@ -1881,15 +1755,14 @@
}
@Test public void redirectedOnHttps() throws IOException, InterruptedException {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: /foo")
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("This is the new location!"));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/"));
assertEquals("This is the new location!",
readAscii(connection.getInputStream(), Integer.MAX_VALUE));
@@ -1902,15 +1775,14 @@
}
@Test public void notRedirectedFromHttpsToHttp() throws IOException, InterruptedException {
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: http://anyhost/foo")
.setBody("This page has moved!"));
- server.play();
- client.setFollowProtocolRedirects(false);
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setFollowSslRedirects(false);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/"));
assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
}
@@ -1919,27 +1791,23 @@
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: https://anyhost/foo")
.setBody("This page has moved!"));
- server.play();
- client.setFollowProtocolRedirects(false);
+ client.client().setFollowSslRedirects(false);
connection = client.open(server.getUrl("/"));
assertEquals("This page has moved!", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
}
@Test public void redirectedFromHttpsToHttpFollowingProtocolRedirects() throws Exception {
- server2 = new MockWebServer();
server2.enqueue(new MockResponse().setBody("This is insecure HTTP!"));
- server2.play();
- server.useHttps(sslContext.getSocketFactory(), false);
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server2.getUrl("/"))
.setBody("This page has moved!"));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- client.setFollowProtocolRedirects(true);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setFollowSslRedirects(true);
HttpsURLConnection connection = (HttpsURLConnection) client.open(server.getUrl("/"));
assertContent("This is insecure HTTP!", connection);
assertNull(connection.getCipherSuite());
@@ -1950,19 +1818,16 @@
}
@Test public void redirectedFromHttpToHttpsFollowingProtocolRedirects() throws Exception {
- server2 = new MockWebServer();
- server2.useHttps(sslContext.getSocketFactory(), false);
+ server2.get().useHttps(sslContext.getSocketFactory(), false);
server2.enqueue(new MockResponse().setBody("This is secure HTTPS!"));
- server2.play();
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server2.getUrl("/"))
.setBody("This page has moved!"));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- client.setFollowProtocolRedirects(true);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setFollowSslRedirects(true);
connection = client.open(server.getUrl("/"));
assertContent("This is secure HTTPS!", connection);
assertFalse(connection instanceof HttpsURLConnection);
@@ -1977,24 +1842,21 @@
}
private void redirectToAnotherOriginServer(boolean https) throws Exception {
- server2 = new MockWebServer();
if (https) {
- server.useHttps(sslContext.getSocketFactory(), false);
- server2.useHttps(sslContext.getSocketFactory(), false);
- server2.setNpnEnabled(false);
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server2.get().useHttps(sslContext.getSocketFactory(), false);
+ server2.get().setProtocolNegotiationEnabled(false);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
}
server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
server2.enqueue(new MockResponse().setBody("This is the 2nd server, again!"));
- server2.play();
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server2.getUrl("/").toString())
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("This is the first server again!"));
- server.play();
connection = client.open(server.getUrl("/"));
assertContent("This is the 2nd server!", connection);
@@ -2004,42 +1866,56 @@
assertContent("This is the first server again!", client.open(server.getUrl("/")));
assertContent("This is the 2nd server, again!", client.open(server2.getUrl("/")));
- String server1Host = hostName + ":" + server.getPort();
- String server2Host = hostName + ":" + server2.getPort();
- assertContains(server.takeRequest().getHeaders(), "Host: " + server1Host);
- assertContains(server2.takeRequest().getHeaders(), "Host: " + server2Host);
+ String server1Host = server.get().getHostName() + ":" + server.getPort();
+ String server2Host = server2.get().getHostName() + ":" + server2.getPort();
+ assertEquals(server1Host, server.takeRequest().getHeader("Host"));
+ assertEquals(server2Host, server2.takeRequest().getHeader("Host"));
assertEquals("Expected connection reuse", 1, server.takeRequest().getSequenceNumber());
assertEquals("Expected connection reuse", 1, server2.takeRequest().getSequenceNumber());
}
@Test public void redirectWithProxySelector() throws Exception {
final List<URI> proxySelectionRequests = new ArrayList<URI>();
- client.setProxySelector(new ProxySelector() {
+ client.client().setProxySelector(new ProxySelector() {
@Override public List<Proxy> select(URI uri) {
proxySelectionRequests.add(uri);
- MockWebServer proxyServer = (uri.getPort() == server.getPort()) ? server : server2;
+ MockWebServer proxyServer = (uri.getPort() == server.get().getPort())
+ ? server.get()
+ : server2.get();
return Arrays.asList(proxyServer.toProxyAddress());
}
+
@Override public void connectFailed(URI uri, SocketAddress address, IOException failure) {
throw new AssertionError();
}
});
- server2 = new MockWebServer();
server2.enqueue(new MockResponse().setBody("This is the 2nd server!"));
- server2.play();
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: " + server2.getUrl("/b").toString())
.setBody("This page has moved!"));
- server.play();
assertContent("This is the 2nd server!", client.open(server.getUrl("/a")));
assertEquals(Arrays.asList(server.getUrl("/a").toURI(), server2.getUrl("/b").toURI()),
proxySelectionRequests);
+ }
- server2.shutdown();
+ @Test public void redirectWithAuthentication() throws Exception {
+ server2.enqueue(new MockResponse().setBody("Page 2"));
+
+ server.enqueue(new MockResponse().setResponseCode(401));
+ server.enqueue(new MockResponse().setResponseCode(302)
+ .addHeader("Location: " + server2.getUrl("/b")));
+
+ client.client().setAuthenticator(
+ new RecordingOkAuthenticator(Credentials.basic("jesse", "secret")));
+ assertContent("Page 2", client.open(server.getUrl("/a")));
+
+ RecordedRequest redirectRequest = server2.takeRequest();
+ assertNull(redirectRequest.getHeader("Authorization"));
+ assertEquals("/b", redirectRequest.getPath());
}
@Test public void response300MultipleChoiceWithPost() throws Exception {
@@ -2073,7 +1949,6 @@
.addHeader("Location: /page2")
.setBody("This page has moved!"));
server.enqueue(new MockResponse().setBody("Page 2"));
- server.play();
connection = client.open(server.getUrl("/page1"));
connection.setDoOutput(true);
@@ -2087,18 +1962,16 @@
RecordedRequest page1 = server.takeRequest();
assertEquals("POST /page1 HTTP/1.1", page1.getRequestLine());
- assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
+ assertEquals("ABCD", page1.getBody().readUtf8());
RecordedRequest page2 = server.takeRequest();
assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
}
@Test public void redirectedPostStripsRequestBodyHeaders() throws Exception {
- server.enqueue(new MockResponse()
- .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: /page2"));
server.enqueue(new MockResponse().setBody("Page 2"));
- server.play();
connection = client.open(server.getUrl("/page1"));
connection.setDoOutput(true);
@@ -2114,13 +1987,12 @@
RecordedRequest page2 = server.takeRequest();
assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
- assertContainsNoneMatching(page2.getHeaders(), "Content-Length");
- assertContains(page2.getHeaders(), "Content-Type: text/plain; charset=utf-8");
- assertContains(page2.getHeaders(), "Transfer-Encoding: identity");
+ assertNull(page2.getHeader("Content-Length"));
+ assertNull(page2.getHeader("Content-Type"));
+ assertNull(page2.getHeader("Transfer-Encoding"));
}
@Test public void response305UseProxy() throws Exception {
- server.play();
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_USE_PROXY)
.addHeader("Location: " + server.getUrl("/"))
.setBody("This page has moved!"));
@@ -2136,31 +2008,46 @@
}
@Test public void response307WithGet() throws Exception {
- test307Redirect("GET");
+ testRedirect(true, "GET");
}
@Test public void response307WithHead() throws Exception {
- test307Redirect("HEAD");
+ testRedirect(true, "HEAD");
}
@Test public void response307WithOptions() throws Exception {
- test307Redirect("OPTIONS");
+ testRedirect(true, "OPTIONS");
}
@Test public void response307WithPost() throws Exception {
- test307Redirect("POST");
+ testRedirect(true, "POST");
}
- private void test307Redirect(String method) throws Exception {
+ @Test public void response308WithGet() throws Exception {
+ testRedirect(false, "GET");
+ }
+
+ @Test public void response308WithHead() throws Exception {
+ testRedirect(false, "HEAD");
+ }
+
+ @Test public void response308WithOptions() throws Exception {
+ testRedirect(false, "OPTIONS");
+ }
+
+ @Test public void response308WithPost() throws Exception {
+ testRedirect(false, "POST");
+ }
+
+ private void testRedirect(boolean temporary, String method) throws Exception {
MockResponse response1 = new MockResponse()
- .setResponseCode(HTTP_TEMP_REDIRECT)
+ .setResponseCode(temporary ? HTTP_TEMP_REDIRECT : HTTP_PERM_REDIRECT)
.addHeader("Location: /page2");
if (!method.equals("HEAD")) {
response1.setBody("This page has moved!");
}
server.enqueue(response1);
server.enqueue(new MockResponse().setBody("Page 2"));
- server.play();
connection = client.open(server.getUrl("/page1"));
connection.setRequestMethod(method);
@@ -2179,13 +2066,13 @@
if (method.equals("GET")) {
assertEquals("Page 2", response);
- } else if (method.equals("HEAD")) {
+ } else if (method.equals("HEAD")) {
assertEquals("", response);
} else {
// Methods other than GET/HEAD shouldn't follow the redirect
if (method.equals("POST")) {
assertTrue(connection.getDoOutput());
- assertEquals(Arrays.toString(requestBody), Arrays.toString(page1.getBody()));
+ assertEquals("ABCD", page1.getBody().readUtf8());
}
assertEquals(1, server.getRequestCount());
assertEquals("This page has moved!", response);
@@ -2206,7 +2093,6 @@
.setBody("Redirecting to /" + (i + 1)));
}
server.enqueue(new MockResponse().setBody("Success!"));
- server.play();
connection = client.open(server.getUrl("/0"));
assertContent("Success!", connection);
@@ -2219,7 +2105,6 @@
.addHeader("Location: /" + (i + 1))
.setBody("Redirecting to /" + (i + 1)));
}
- server.play();
connection = client.open(server.getUrl("/0"));
try {
@@ -2227,7 +2112,7 @@
fail();
} catch (ProtocolException expected) {
assertEquals(HttpURLConnection.HTTP_MOVED_TEMP, connection.getResponseCode());
- assertEquals("Too many redirects: 21", expected.getMessage());
+ assertEquals("Too many follow-up requests: 21", expected.getMessage());
assertContent("Redirecting to /21", connection);
assertEquals(server.getUrl("/20"), connection.getURL());
}
@@ -2239,21 +2124,22 @@
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
- client.setHostnameVerifier(hostnameVerifier);
- client.setSslSocketFactory(sc.getSocketFactory());
- server.useHttps(sslContext.getSocketFactory(), false);
+ client.client().setHostnameVerifier(hostnameVerifier);
+ client.client().setSslSocketFactory(sc.getSocketFactory());
+ server.get().useHttps(sslContext.getSocketFactory(), false);
server.enqueue(new MockResponse().setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
server.enqueue(new MockResponse().setBody("GHI"));
- server.play();
URL url = server.getUrl("/");
assertContent("ABC", client.open(url));
assertContent("DEF", client.open(url));
assertContent("GHI", client.open(url));
- assertEquals(Arrays.asList("verify " + hostName), hostnameVerifier.calls);
- assertEquals(Arrays.asList("checkServerTrusted [CN=" + hostName + " 1]"), trustManager.calls);
+ assertEquals(Arrays.asList("verify " + server.get().getHostName()),
+ hostnameVerifier.calls);
+ assertEquals(Arrays.asList("checkServerTrusted [CN=" + server.get().getHostName() + " 1]"),
+ trustManager.calls);
}
@Test public void readTimeouts() throws IOException {
@@ -2264,7 +2150,6 @@
new MockResponse().setBody("ABC").clearHeaders().addHeader("Content-Length: 4");
server.enqueue(timeout);
server.enqueue(new MockResponse().setBody("unused")); // to keep the server alive
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
connection.setReadTimeout(1000);
@@ -2275,13 +2160,48 @@
try {
in.read(); // if Content-Length was accurate, this would return -1 immediately
fail();
- } catch (SocketTimeoutException expected) {
+ } catch (IOException expected) {
+ }
+ }
+
+ /** Confirm that an unacknowledged write times out. */
+ @Test public void writeTimeouts() throws IOException {
+ // Sockets on some platforms can have large buffers that mean writes do not block when
+ // required. These socket factories explicitly set the buffer sizes on sockets created.
+ final int SOCKET_BUFFER_SIZE = 256 * 1024;
+ server.get().setServerSocketFactory(
+ new DelegatingServerSocketFactory(ServerSocketFactory.getDefault()) {
+ @Override
+ protected void configureServerSocket(ServerSocket serverSocket) throws IOException {
+ serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ }
+ });
+ client.client().setSocketFactory(new DelegatingSocketFactory(SocketFactory.getDefault()) {
+ @Override
+ protected void configureSocket(Socket socket) throws IOException {
+ socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ }
+ });
+
+ server.enqueue(new MockResponse()
+ .throttleBody(1, 1, TimeUnit.SECONDS)); // Prevent the server from reading!
+
+ client.client().setWriteTimeout(500, TimeUnit.MILLISECONDS);
+ connection = client.open(server.getUrl("/"));
+ connection.setDoOutput(true);
+ connection.setChunkedStreamingMode(0);
+ OutputStream out = connection.getOutputStream();
+ try {
+ byte[] data = new byte[16 * 1024 * 1024]; // 16 MiB.
+ out.write(data);
+ fail();
+ } catch (IOException expected) {
}
}
@Test public void setChunkedEncodingAsRequestProperty() throws IOException, InterruptedException {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
connection.setRequestProperty("Transfer-encoding", "chunked");
@@ -2290,13 +2210,12 @@
assertEquals(200, connection.getResponseCode());
RecordedRequest request = server.takeRequest();
- assertEquals("ABC", new String(request.getBody(), "UTF-8"));
+ assertEquals("ABC", request.getBody().readUtf8());
}
@Test public void connectionCloseInRequest() throws IOException, InterruptedException {
server.enqueue(new MockResponse()); // server doesn't honor the connection: close header!
server.enqueue(new MockResponse());
- server.play();
HttpURLConnection a = client.open(server.getUrl("/"));
a.setRequestProperty("Connection", "close");
@@ -2313,7 +2232,6 @@
@Test public void connectionCloseInResponse() throws IOException, InterruptedException {
server.enqueue(new MockResponse().addHeader("Connection: close"));
server.enqueue(new MockResponse());
- server.play();
HttpURLConnection a = client.open(server.getUrl("/"));
assertEquals(200, a.getResponseCode());
@@ -2332,7 +2250,6 @@
.addHeader("Connection: close");
server.enqueue(response);
server.enqueue(new MockResponse().setBody("This is the new location!"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
assertEquals("This is the new location!",
@@ -2352,7 +2269,6 @@
.setSocketPolicy(SHUTDOWN_INPUT_AT_END)
.addHeader("Location: /foo"));
server.enqueue(new MockResponse().setBody("This is the new page!"));
- server.play();
assertContent("This is the new page!", client.open(server.getUrl("/")));
@@ -2363,7 +2279,6 @@
@Test public void responseCodeDisagreesWithHeaders() throws IOException, InterruptedException {
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NO_CONTENT)
.setBody("This body is not allowed!"));
- server.play();
URLConnection connection = client.open(server.getUrl("/"));
assertEquals("This body is not allowed!",
@@ -2371,8 +2286,7 @@
}
@Test public void singleByteReadIsSigned() throws IOException {
- server.enqueue(new MockResponse().setBody(new byte[] {-2, -1}));
- server.play();
+ server.enqueue(new MockResponse().setBody(new Buffer().writeByte(-2).writeByte(-1)));
connection = client.open(server.getUrl("/"));
InputStream in = connection.getInputStream();
@@ -2400,7 +2314,6 @@
*/
private void testFlushAfterStreamTransmitted(TransferKind transferKind) throws IOException {
server.enqueue(new MockResponse().setBody("abc"));
- server.play();
connection = client.open(server.getUrl("/"));
connection.setDoOutput(true);
@@ -2425,13 +2338,7 @@
}
@Test public void getHeadersThrows() throws IOException {
- // Enqueue a response for every IP address held by localhost, because the route selector
- // will try each in sequence.
- // TODO: use the fake Dns implementation instead of a loop
- for (InetAddress inetAddress : InetAddress.getAllByName(server.getHostName())) {
- server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
- }
- server.play();
+ server.enqueue(new MockResponse().setSocketPolicy(DISCONNECT_AT_START));
connection = client.open(server.getUrl("/"));
try {
@@ -2466,16 +2373,14 @@
}
@Test public void getKeepAlive() throws Exception {
- MockWebServer server = new MockWebServer();
server.enqueue(new MockResponse().setBody("ABC"));
- server.play();
// The request should work once and then fail
HttpURLConnection connection1 = client.open(server.getUrl(""));
connection1.setReadTimeout(100);
InputStream input = connection1.getInputStream();
assertEquals("ABC", readAscii(input, Integer.MAX_VALUE));
- server.shutdown();
+ server.get().shutdown();
try {
HttpURLConnection connection2 = client.open(server.getUrl(""));
connection2.setReadTimeout(100);
@@ -2485,45 +2390,12 @@
}
}
- /** Don't explode if the cache returns a null body. http://b/3373699 */
- @Test public void responseCacheReturnsNullOutputStream() throws Exception {
- final AtomicBoolean aborted = new AtomicBoolean();
- client.setResponseCache(new ResponseCache() {
- @Override public CacheResponse get(URI uri, String requestMethod,
- Map<String, List<String>> requestHeaders) throws IOException {
- return null;
- }
-
- @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
- return new CacheRequest() {
- @Override public void abort() {
- aborted.set(true);
- }
-
- @Override public OutputStream getBody() throws IOException {
- return null;
- }
- };
- }
- });
-
- server.enqueue(new MockResponse().setBody("abcdef"));
- server.play();
-
- HttpURLConnection connection = client.open(server.getUrl("/"));
- InputStream in = connection.getInputStream();
- assertEquals("abc", readAscii(in, 3));
- in.close();
- assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
- }
-
/** http://code.google.com/p/android/issues/detail?id=14562 */
@Test public void readAfterLastByte() throws Exception {
server.enqueue(new MockResponse().setBody("ABC")
.clearHeaders()
.addHeader("Connection: close")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
- server.play();
connection = client.open(server.getUrl("/"));
InputStream in = connection.getInputStream();
@@ -2534,7 +2406,6 @@
@Test public void getContent() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
InputStream in = (InputStream) connection.getContent();
assertEquals("A", readAscii(in, Integer.MAX_VALUE));
@@ -2542,7 +2413,6 @@
@Test public void getContentOfType() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
try {
connection.getContent(null);
@@ -2554,12 +2424,11 @@
fail();
} catch (NullPointerException expected) {
}
- assertNull(connection.getContent(new Class[] {getClass()}));
+ assertNull(connection.getContent(new Class[]{getClass()}));
}
@Test public void getOutputStreamOnGetFails() throws Exception {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
try {
connection.getOutputStream();
@@ -2570,7 +2439,6 @@
@Test public void getOutputAfterGetInputStreamFails() throws Exception {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
connection.setDoOutput(true);
try {
@@ -2583,7 +2451,6 @@
@Test public void setDoOutputOrDoInputAfterConnectFails() throws Exception {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
connection.connect();
try {
@@ -2600,7 +2467,6 @@
@Test public void clientSendsContentLength() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/"));
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
@@ -2608,26 +2474,23 @@
out.close();
assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
- assertContains(request.getHeaders(), "Content-Length: 3");
+ assertEquals("3", request.getHeader("Content-Length"));
}
@Test public void getContentLengthConnects() throws Exception {
server.enqueue(new MockResponse().setBody("ABC"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals(3, connection.getContentLength());
}
@Test public void getContentTypeConnects() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("ABC"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals("text/plain", connection.getContentType());
}
@Test public void getContentEncodingConnects() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Encoding: identity").setBody("ABC"));
- server.play();
connection = client.open(server.getUrl("/"));
assertEquals("identity", connection.getContentEncoding());
}
@@ -2635,13 +2498,24 @@
// http://b/4361656
@Test public void urlContainsQueryButNoPath() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- URL url = new URL("http", server.getHostName(), server.getPort(), "?query");
+
+ URL url = new URL("http", server.get().getHostName(), server.getPort(), "?query");
assertEquals("A", readAscii(client.open(url).getInputStream(), Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
assertEquals("GET /?query HTTP/1.1", request.getRequestLine());
}
+ @Test public void doOutputForMethodThatDoesntSupportOutput() throws Exception {
+ connection = client.open(server.getUrl("/"));
+ connection.setRequestMethod("HEAD");
+ connection.setDoOutput(true);
+ try {
+ connection.connect();
+ fail();
+ } catch (IOException expected) {
+ }
+ }
+
// http://code.google.com/p/android/issues/detail?id=20442
@Test public void inputStreamAvailableWithChunkedEncoding() throws Exception {
testInputStreamAvailable(TransferKind.CHUNKED);
@@ -2660,7 +2534,6 @@
MockResponse response = new MockResponse();
transferKind.setBody(response, body, 4);
server.enqueue(response);
- server.play();
connection = client.open(server.getUrl("/"));
InputStream in = connection.getInputStream();
for (int i = 0; i < body.length(); i++) {
@@ -2698,10 +2571,9 @@
private void reusedConnectionFailsWithPost(TransferKind transferKind, int requestSize)
throws Exception {
- server.enqueue(new MockResponse().setBody("A").setSocketPolicy(SHUTDOWN_INPUT_AT_END));
+ server.enqueue(new MockResponse().setBody("A").setSocketPolicy(DISCONNECT_AT_END));
server.enqueue(new MockResponse().setBody("B"));
server.enqueue(new MockResponse().setBody("C"));
- server.play();
assertContent("A", client.open(server.getUrl("/a")));
@@ -2722,12 +2594,36 @@
assertEquals("/a", requestA.getPath());
RecordedRequest requestB = server.takeRequest();
assertEquals("/b", requestB.getPath());
- assertEquals(Arrays.toString(requestBody), Arrays.toString(requestB.getBody()));
+ assertEquals(Arrays.toString(requestBody), Arrays.toString(requestB.getBody().readByteArray()));
+ }
+
+ @Test public void postBodyRetransmittedOnFailureRecovery() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
+ server.enqueue(new MockResponse().setBody("def"));
+
+ // Seed the connection pool so we have something that can fail.
+ assertContent("abc", client.open(server.getUrl("/")));
+
+ HttpURLConnection post = client.open(server.getUrl("/"));
+ post.setDoOutput(true);
+ post.getOutputStream().write("body!".getBytes(Util.UTF_8));
+ assertContent("def", post);
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals(0, get.getSequenceNumber());
+
+ RecordedRequest post1 = server.takeRequest();
+ assertEquals("body!", post1.getBody().readUtf8());
+ assertEquals(1, post1.getSequenceNumber());
+
+ RecordedRequest post2 = server.takeRequest();
+ assertEquals("body!", post2.getBody().readUtf8());
+ assertEquals(0, post2.getSequenceNumber());
}
@Test public void fullyBufferedPostIsTooShort() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/b"));
connection.setRequestProperty("Content-Length", "4");
@@ -2745,7 +2641,6 @@
@Test public void fullyBufferedPostIsTooLong() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
connection = client.open(server.getUrl("/b"));
connection.setRequestProperty("Content-Length", "3");
@@ -2788,7 +2683,6 @@
@Test public void emptyRequestHeaderValueIsAllowed() throws Exception {
server.enqueue(new MockResponse().setBody("body"));
- server.play();
connection = client.open(server.getUrl("/"));
connection.addRequestProperty("B", "");
assertContent("body", connection);
@@ -2797,7 +2691,6 @@
@Test public void emptyResponseHeaderValueIsAllowed() throws Exception {
server.enqueue(new MockResponse().addHeader("A:").setBody("body"));
- server.play();
connection = client.open(server.getUrl("/"));
assertContent("body", connection);
assertEquals("", connection.getHeaderField("A"));
@@ -2805,7 +2698,6 @@
@Test public void emptyRequestHeaderNameIsStrict() throws Exception {
server.enqueue(new MockResponse().setBody("body"));
- server.play();
connection = client.open(server.getUrl("/"));
try {
connection.setRequestProperty("", "A");
@@ -2815,8 +2707,9 @@
}
@Test public void emptyResponseHeaderNameIsLenient() throws Exception {
- server.enqueue(new MockResponse().addHeader(":A").setBody("body"));
- server.play();
+ Headers.Builder headers = new Headers.Builder();
+ Internal.instance.addLenient(headers, ":A");
+ server.enqueue(new MockResponse().setHeaders(headers.build()).setBody("body"));
connection = client.open(server.getUrl("/"));
connection.getResponseCode();
assertEquals("A", connection.getHeaderField(""));
@@ -2834,61 +2727,142 @@
fail("TODO");
}
- @Test public void customAuthenticator() throws Exception {
+ @Test public void customBasicAuthenticator() throws Exception {
MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401)
.addHeader("WWW-Authenticate: Basic realm=\"protected area\"")
.setBody("Please authenticate.");
server.enqueue(pleaseAuthenticate);
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- Credential credential = Credential.basic("jesse", "peanutbutter");
+ String credential = Credentials.basic("jesse", "peanutbutter");
RecordingOkAuthenticator authenticator = new RecordingOkAuthenticator(credential);
- client.setAuthenticator(authenticator);
+ client.client().setAuthenticator(authenticator);
assertContent("A", client.open(server.getUrl("/private")));
- assertContainsNoneMatching(server.takeRequest().getHeaders(), "Authorization: .*");
- assertContains(server.takeRequest().getHeaders(),
- "Authorization: " + credential.getHeaderValue());
+ assertNull(server.takeRequest().getHeader("Authorization"));
+ assertEquals(credential, server.takeRequest().getHeader("Authorization"));
assertEquals(Proxy.NO_PROXY, authenticator.onlyProxy());
- URL url = authenticator.onlyUrl();
- assertEquals("/private", url.getPath());
- assertEquals(Arrays.asList(new Challenge("Basic", "protected area")),
- authenticator.onlyChallenge());
+ Response response = authenticator.onlyResponse();
+ assertEquals("/private", response.request().url().getPath());
+ assertEquals(Arrays.asList(new Challenge("Basic", "protected area")), response.challenges());
}
- @Test public void npnSetsProtocolHeader_SPDY_3() throws Exception {
- npnSetsProtocolHeader(Protocol.SPDY_3);
- }
-
- @Test public void npnSetsProtocolHeader_HTTP_2() throws Exception {
- npnSetsProtocolHeader(Protocol.HTTP_2);
- }
-
- private void npnSetsProtocolHeader(Protocol protocol) throws IOException {
- enableNpn(protocol);
+ @Test public void customTokenAuthenticator() throws Exception {
+ MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(401)
+ .addHeader("WWW-Authenticate: Bearer realm=\"oauthed\"")
+ .setBody("Please authenticate.");
+ server.enqueue(pleaseAuthenticate);
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- client.setProtocols(Arrays.asList(Protocol.HTTP_11, protocol));
+
+ RecordingOkAuthenticator authenticator = new RecordingOkAuthenticator("oauthed abc123");
+ client.client().setAuthenticator(authenticator);
+ assertContent("A", client.open(server.getUrl("/private")));
+
+ assertNull(server.takeRequest().getHeader("Authorization"));
+ assertEquals("oauthed abc123", server.takeRequest().getHeader("Authorization"));
+
+ Response response = authenticator.onlyResponse();
+ assertEquals("/private", response.request().url().getPath());
+ assertEquals(Arrays.asList(new Challenge("Bearer", "oauthed")), response.challenges());
+ }
+
+ @Test public void authenticateCallsTrackedAsRedirects() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(302)
+ .addHeader("Location: /b"));
+ server.enqueue(new MockResponse()
+ .setResponseCode(401)
+ .addHeader("WWW-Authenticate: Basic realm=\"protected area\""));
+ server.enqueue(new MockResponse().setBody("c"));
+
+ RecordingOkAuthenticator authenticator = new RecordingOkAuthenticator(
+ Credentials.basic("jesse", "peanutbutter"));
+ client.client().setAuthenticator(authenticator);
+ assertContent("c", client.open(server.getUrl("/a")));
+
+ Response challengeResponse = authenticator.responses.get(0);
+ assertEquals("/b", challengeResponse.request().url().getPath());
+
+ Response redirectedBy = challengeResponse.priorResponse();
+ assertEquals("/a", redirectedBy.request().url().getPath());
+ }
+
+ @Test public void attemptAuthorization20Times() throws Exception {
+ for (int i = 0; i < 20; i++) {
+ server.enqueue(new MockResponse().setResponseCode(401));
+ }
+ server.enqueue(new MockResponse().setBody("Success!"));
+
+ String credential = Credentials.basic("jesse", "peanutbutter");
+ client.client().setAuthenticator(new RecordingOkAuthenticator(credential));
+
+ connection = client.open(server.getUrl("/0"));
+ assertContent("Success!", connection);
+ }
+
+ @Test public void doesNotAttemptAuthorization21Times() throws Exception {
+ for (int i = 0; i < 21; i++) {
+ server.enqueue(new MockResponse().setResponseCode(401));
+ }
+
+ String credential = Credentials.basic("jesse", "peanutbutter");
+ client.client().setAuthenticator(new RecordingOkAuthenticator(credential));
+
+ connection = client.open(server.getUrl("/"));
+ try {
+ connection.getInputStream();
+ fail();
+ } catch (ProtocolException expected) {
+ assertEquals(401, connection.getResponseCode());
+ assertEquals("Too many follow-up requests: 21", expected.getMessage());
+ }
+ }
+
+ @Test public void setsNegotiatedProtocolHeader_SPDY_3() throws Exception {
+ setsNegotiatedProtocolHeader(Protocol.SPDY_3);
+ }
+
+ @Test public void setsNegotiatedProtocolHeader_HTTP_2() throws Exception {
+ setsNegotiatedProtocolHeader(Protocol.HTTP_2);
+ }
+
+ private void setsNegotiatedProtocolHeader(Protocol protocol) throws IOException {
+ enableProtocol(protocol);
+ server.enqueue(new MockResponse().setBody("A"));
+ client.client().setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
connection = client.open(server.getUrl("/"));
List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
- assertEquals(Arrays.asList(protocol.name.utf8()), protocolValues);
+ assertEquals(Arrays.asList(protocol.toString()), protocolValues);
assertContent("A", connection);
}
+ @Test public void http10SelectedProtocol() throws Exception {
+ server.enqueue(new MockResponse().setStatus("HTTP/1.0 200 OK"));
+ connection = client.open(server.getUrl("/"));
+ List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
+ assertEquals(Arrays.asList("http/1.0"), protocolValues);
+ }
+
+ @Test public void http11SelectedProtocol() throws Exception {
+ server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 OK"));
+ connection = client.open(server.getUrl("/"));
+ List<String> protocolValues = connection.getHeaderFields().get(SELECTED_PROTOCOL);
+ assertEquals(Arrays.asList("http/1.1"), protocolValues);
+ }
+
/** For example, empty Protobuf RPC messages end up as a zero-length POST. */
@Test public void zeroLengthPost() throws IOException, InterruptedException {
zeroLengthPayload("POST");
}
@Test public void zeroLengthPost_SPDY_3() throws Exception {
- enableNpn(Protocol.SPDY_3);
+ enableProtocol(Protocol.SPDY_3);
zeroLengthPost();
}
@Test public void zeroLengthPost_HTTP_2() throws Exception {
- enableNpn(Protocol.HTTP_2);
+ enableProtocol(Protocol.HTTP_2);
zeroLengthPost();
}
@@ -2898,19 +2872,18 @@
}
@Test public void zeroLengthPut_SPDY_3() throws Exception {
- enableNpn(Protocol.SPDY_3);
+ enableProtocol(Protocol.SPDY_3);
zeroLengthPut();
}
@Test public void zeroLengthPut_HTTP_2() throws Exception {
- enableNpn(Protocol.HTTP_2);
+ enableProtocol(Protocol.HTTP_2);
zeroLengthPut();
}
private void zeroLengthPayload(String method)
throws IOException, InterruptedException {
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
connection.setRequestProperty("Content-Length", "0");
connection.setRequestMethod(method);
@@ -2923,16 +2896,29 @@
assertEquals(0L, zeroLengthPayload.getBodySize());
}
+ @Test public void unspecifiedRequestBodyContentTypeGetsDefault() throws Exception {
+ server.enqueue(new MockResponse());
+
+ connection = client.open(server.getUrl("/"));
+ connection.setDoOutput(true);
+ connection.getOutputStream().write("abc".getBytes(UTF_8));
+ assertEquals(200, connection.getResponseCode());
+
+ RecordedRequest request = server.takeRequest();
+ assertEquals("application/x-www-form-urlencoded", request.getHeader("Content-Type"));
+ assertEquals("3", request.getHeader("Content-Length"));
+ assertEquals("abc", request.getBody().readUtf8());
+ }
+
@Test public void setProtocols() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- client.setProtocols(Arrays.asList(Protocol.HTTP_11));
+ client.client().setProtocols(Arrays.asList(Protocol.HTTP_1_1));
assertContent("A", client.open(server.getUrl("/")));
}
@Test public void setProtocolsWithoutHttp11() throws Exception {
try {
- client.setProtocols(Arrays.asList(Protocol.SPDY_3));
+ client.client().setProtocols(Arrays.asList(Protocol.SPDY_3));
fail();
} catch (IllegalArgumentException expected) {
}
@@ -2940,16 +2926,15 @@
@Test public void setProtocolsWithNull() throws Exception {
try {
- client.setProtocols(Arrays.asList(Protocol.HTTP_11, null));
+ client.client().setProtocols(Arrays.asList(Protocol.HTTP_1_1, null));
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void veryLargeFixedLengthRequest() throws Exception {
- server.setBodyLimit(0);
+ server.get().setBodyLimit(0);
server.enqueue(new MockResponse());
- server.play();
connection = client.open(server.getUrl("/"));
connection.setDoOutput(true);
@@ -2981,8 +2966,6 @@
.addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse().setBody("b"));
- server.play();
-
HttpURLConnection connection1 = client.open(server.getUrl("/"));
assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection1.getResponseCode());
assertContent("", connection1);
@@ -3007,9 +2990,8 @@
.setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
.addHeader("Location: /foo")
.addHeader("Content-Encoding: gzip")
- .setBody(gzip("Moved! Moved! Moved!".getBytes(UTF_8))));
+ .setBody(gzip("Moved! Moved! Moved!")));
server.enqueue(new MockResponse().setBody("This is the new page!"));
- server.play();
HttpURLConnection connection = client.open(server.getUrl("/"));
assertContent("This is the new page!", connection);
@@ -3022,45 +3004,11 @@
}
/**
- * Tolerate bad https proxy response when using HttpResponseCache. Android bug 6754912.
- */
- @Test
- public void testConnectViaHttpProxyToHttpsUsingBadProxyAndHttpResponseCache() throws Exception {
- initResponseCache();
-
- server.useHttps(sslContext.getSocketFactory(), true);
- // The inclusion of a body in the response to a CONNECT is key to reproducing b/6754912.
- MockResponse
- badProxyResponse = new MockResponse()
- .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
- .clearHeaders()
- .setBody("bogus proxy connect response content");
-
- server.enqueue(badProxyResponse);
- server.enqueue(new MockResponse().setBody("response"));
-
- server.play();
-
- URL url = new URL("https://android.com/foo");
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
- ProxyConfig proxyConfig = ProxyConfig.PROXY_SYSTEM_PROPERTY;
- HttpsURLConnection connection = (HttpsURLConnection) proxyConfig.connect(server, client, url);
- assertContent("response", connection);
-
- RecordedRequest connect = server.takeRequest();
- assertEquals("CONNECT android.com:443 HTTP/1.1", connect.getRequestLine());
- assertContains(connect.getHeaders(), "Host: android.com");
- }
-
- /**
* The RFC is unclear in this regard as it only specifies that this should
* invalidate the cache entry (if any).
*/
@Test public void bodyPermittedOnDelete() throws Exception {
server.enqueue(new MockResponse());
- server.play();
HttpURLConnection connection = client.open(server.getUrl("/"));
connection.setRequestMethod("DELETE");
@@ -3070,16 +3018,48 @@
RecordedRequest request = server.takeRequest();
assertEquals("DELETE", request.getMethod());
- assertEquals("BODY", new String(request.getBody(), UTF_8));
+ assertEquals("BODY", request.getBody().readUtf8());
+ }
+
+ @Test public void userAgentPicksUpHttpAgentSystemProperty() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ System.setProperty("http.agent", "foo");
+ assertContent("abc", client.open(server.getUrl("/")));
+
+ RecordedRequest request = server.takeRequest();
+ assertEquals("foo", request.getHeader("User-Agent"));
+ }
+
+ @Test public void userAgentDefaultsToJavaVersion() throws Exception {
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ assertContent("abc", client.open(server.getUrl("/")));
+
+ RecordedRequest request = server.takeRequest();
+ assertTrue(request.getHeader("User-Agent").startsWith("Java"));
+ }
+
+ @Test public void interceptorsNotInvoked() throws Exception {
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ throw new AssertionError();
+ }
+ };
+ client.client().interceptors().add(interceptor);
+ client.client().networkInterceptors().add(interceptor);
+
+ server.enqueue(new MockResponse().setBody("abc"));
+ assertContent("abc", client.open(server.getUrl("/")));
}
/** Returns a gzipped copy of {@code bytes}. */
- public byte[] gzip(byte[] bytes) throws IOException {
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
- gzippedOut.write(bytes);
- gzippedOut.close();
- return bytesOut.toByteArray();
+ public Buffer gzip(String data) throws IOException {
+ Buffer result = new Buffer();
+ BufferedSink gzipSink = Okio.buffer(new GzipSink(result));
+ gzipSink.writeUtf8(data);
+ gzipSink.close();
+ return result;
}
/**
@@ -3096,111 +3076,98 @@
assertContent(expected, connection, Integer.MAX_VALUE);
}
- private void assertContains(List<String> headers, String header) {
- assertTrue(headers.toString(), headers.contains(header));
- }
-
- private void assertContainsNoneMatching(List<String> headers, String pattern) {
- for (String header : headers) {
- if (header.matches(pattern)) {
- fail("Header " + header + " matches " + pattern);
- }
- }
- }
-
private Set<String> newSet(String... elements) {
return new HashSet<String>(Arrays.asList(elements));
}
enum TransferKind {
CHUNKED() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize)
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize)
throws IOException {
response.setChunkedBody(content, chunkSize);
}
-
@Override void setForRequest(HttpURLConnection connection, int contentLength) {
connection.setChunkedStreamingMode(5);
}
},
FIXED_LENGTH() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
}
-
@Override void setForRequest(HttpURLConnection connection, int contentLength) {
connection.setFixedLengthStreamingMode(contentLength);
}
},
END_OF_STREAM() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
response.setSocketPolicy(DISCONNECT_AT_END);
- for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
- if (h.next().startsWith("Content-Length:")) {
- h.remove();
- break;
- }
- }
+ response.removeHeader("Content-Length");
}
@Override void setForRequest(HttpURLConnection connection, int contentLength) {
}
};
- abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException;
+ abstract void setBody(MockResponse response, Buffer content, int chunkSize) throws IOException;
abstract void setForRequest(HttpURLConnection connection, int contentLength);
void setBody(MockResponse response, String content, int chunkSize) throws IOException {
- setBody(response, content.getBytes("UTF-8"), chunkSize);
+ setBody(response, new Buffer().writeUtf8(content), chunkSize);
}
}
enum ProxyConfig {
NO_PROXY() {
- @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+ @Override public HttpURLConnection connect(
+ MockWebServer server, OkUrlFactory streamHandlerFactory, URL url)
throws IOException {
- client.setProxy(Proxy.NO_PROXY);
- return client.open(url);
+ streamHandlerFactory.client().setProxy(Proxy.NO_PROXY);
+ return streamHandlerFactory.open(url);
}
},
CREATE_ARG() {
- @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+ @Override public HttpURLConnection connect(
+ MockWebServer server, OkUrlFactory streamHandlerFactory, URL url)
throws IOException {
- client.setProxy(server.toProxyAddress());
- return client.open(url);
+ streamHandlerFactory.client().setProxy(server.toProxyAddress());
+ return streamHandlerFactory.open(url);
}
},
PROXY_SYSTEM_PROPERTY() {
- @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+ @Override public HttpURLConnection connect(
+ MockWebServer server, OkUrlFactory streamHandlerFactory, URL url)
throws IOException {
- System.setProperty("proxyHost", "localhost");
+ System.setProperty("proxyHost", server.getHostName());
System.setProperty("proxyPort", Integer.toString(server.getPort()));
- return client.open(url);
+ return streamHandlerFactory.open(url);
}
},
HTTP_PROXY_SYSTEM_PROPERTY() {
- @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+ @Override public HttpURLConnection connect(
+ MockWebServer server, OkUrlFactory streamHandlerFactory, URL url)
throws IOException {
- System.setProperty("http.proxyHost", "localhost");
+ System.setProperty("http.proxyHost", server.getHostName());
System.setProperty("http.proxyPort", Integer.toString(server.getPort()));
- return client.open(url);
+ return streamHandlerFactory.open(url);
}
},
HTTPS_PROXY_SYSTEM_PROPERTY() {
- @Override public HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+ @Override public HttpURLConnection connect(
+ MockWebServer server, OkUrlFactory streamHandlerFactory, URL url)
throws IOException {
- System.setProperty("https.proxyHost", "localhost");
+ System.setProperty("https.proxyHost", server.getHostName());
System.setProperty("https.proxyPort", Integer.toString(server.getPort()));
- return client.open(url);
+ return streamHandlerFactory.open(url);
}
};
- public abstract HttpURLConnection connect(MockWebServer server, OkHttpClient client, URL url)
+ public abstract HttpURLConnection connect(
+ MockWebServer server, OkUrlFactory streamHandlerFactory, URL url)
throws IOException;
}
@@ -3231,7 +3198,7 @@
}
private static class FakeProxySelector extends ProxySelector {
- List<Proxy> proxies = new ArrayList<Proxy>();
+ List<Proxy> proxies = new ArrayList<>();
@Override public List<Proxy> select(URI uri) {
// Don't handle 'socket' schemes, which the RI's Socket class may request (for SOCKS).
@@ -3245,14 +3212,25 @@
/**
* Tests that use this will fail unless boot classpath is set. Ex. {@code
- * -Xbootclasspath/p:/tmp/npn-boot-8.1.2.v20120308.jar}
+ * -Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317}
*/
- private void enableNpn(Protocol protocol) {
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
- client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_11));
- server.useHttps(sslContext.getSocketFactory(), false);
- server.setNpnEnabled(true);
- server.setNpnProtocols(client.getProtocols());
+ private void enableProtocol(Protocol protocol) {
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(new RecordingHostnameVerifier());
+ client.client().setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
+ server.get().useHttps(sslContext.getSocketFactory(), false);
+ server.get().setProtocolNegotiationEnabled(true);
+ server.get().setProtocols(client.client().getProtocols());
+ }
+
+ /**
+ * Used during tests that involve TLS connection fallback attempts. OkHttp includes the
+ * TLS_FALLBACK_SCSV cipher on fallback connections. See
+ * {@link com.squareup.okhttp.FallbackTestClientSocketFactory} for details.
+ */
+ private static void suppressTlsFallbackScsv(OkHttpClient client) {
+ FallbackTestClientSocketFactory clientSocketFactory =
+ new FallbackTestClientSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(clientSocketFactory);
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
index 8f70922..d0b5e97 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/BaseTestHandler.java
@@ -30,7 +30,7 @@
@Override
public void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
- int priority, List<Header> headerBlock, HeadersMode headersMode) {
+ List<Header> headerBlock, HeadersMode headersMode) {
fail();
}
@@ -58,7 +58,8 @@
fail();
}
- @Override public void priority(int streamId, int priority) {
+ @Override public void priority(int streamId, int streamDependency, int weight,
+ boolean exclusive) {
fail();
}
@@ -66,4 +67,9 @@
public void pushPromise(int streamId, int associatedStreamId, List<Header> headerBlock) {
fail();
}
+
+ @Override public void alternateService(int streamId, String origin, ByteString protocol,
+ String host, int port, long maxAge) {
+ fail();
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java
deleted file mode 100644
index 42ddcab..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft05Test.java
+++ /dev/null
@@ -1,890 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.spdy;
-
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import okio.ByteString;
-import okio.OkBuffer;
-import org.junit.Before;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.Util.headerEntries;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-
-public class HpackDraft05Test {
-
- private final OkBuffer bytesIn = new OkBuffer();
- private HpackDraft05.Reader hpackReader;
- private OkBuffer bytesOut = new OkBuffer();
- private HpackDraft05.Writer hpackWriter;
-
- @Before public void reset() {
- hpackReader = newReader(bytesIn);
- hpackWriter = new HpackDraft05.Writer(bytesOut);
- }
-
- /**
- * Variable-length quantity special cases strings which are longer than 127
- * bytes. Values such as cookies can be 4KiB, and should be possible to send.
- *
- * <p> http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2
- */
- @Test public void largeHeaderValue() throws IOException {
- char[] value = new char[4096];
- Arrays.fill(value, '!');
- List<Header> headerBlock = headerEntries("cookie", new String(value));
-
- hpackWriter.writeHeaders(headerBlock);
- bytesIn.write(bytesOut, bytesOut.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(0, hpackReader.headerCount);
-
- assertEquals(headerBlock, hpackReader.getAndReset());
- }
-
- /**
- * HPACK has a max header table size, which can be smaller than the max header message.
- * Ensure the larger header content is not lost.
- */
- @Test public void tooLargeToHPackIsStillEmitted() throws IOException {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-key");
-
- out.writeByte(0x0d); // Literal value (len = 13)
- out.writeUtf8("custom-header");
-
- bytesIn.write(out, out.size());
- hpackReader.maxHeaderTableByteCount(1);
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(0, hpackReader.headerCount);
-
- assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndReset());
- }
-
- /** Oldest entries are evicted to support newer ones. */
- @Test public void testEviction() throws IOException {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-foo");
-
- out.writeByte(0x0d); // Literal value (len = 13)
- out.writeUtf8("custom-header");
-
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-bar");
-
- out.writeByte(0x0d); // Literal value (len = 13)
- out.writeUtf8("custom-header");
-
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-baz");
-
- out.writeByte(0x0d); // Literal value (len = 13)
- out.writeUtf8("custom-header");
-
- bytesIn.write(out, out.size());
- // Set to only support 110 bytes (enough for 2 headers).
- hpackReader.maxHeaderTableByteCount(110);
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(2, hpackReader.headerCount);
-
- Header entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, "custom-bar", "custom-header", 55);
- assertHeaderReferenced(headerTableLength() - 1);
-
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, "custom-baz", "custom-header", 55);
- assertHeaderReferenced(headerTableLength() - 2);
-
- // foo isn't here as it is no longer in the table.
- // TODO: emit before eviction?
- assertEquals(headerEntries("custom-bar", "custom-header", "custom-baz", "custom-header"),
- hpackReader.getAndReset());
-
- // Simulate receiving a small settings frame, that implies eviction.
- hpackReader.maxHeaderTableByteCount(55);
- assertEquals(1, hpackReader.headerCount);
- }
-
- /** Header table backing array is initially 8 long, let's ensure it grows. */
- @Test public void dynamicallyGrowsBeyond64Entries() throws IOException {
- OkBuffer out = new OkBuffer();
-
- for (int i = 0; i < 256; i++) {
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-foo");
-
- out.writeByte(0x0d); // Literal value (len = 13)
- out.writeUtf8("custom-header");
- }
-
- bytesIn.write(out, out.size());
- hpackReader.maxHeaderTableByteCount(16384); // Lots of headers need more room!
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(256, hpackReader.headerCount);
- assertHeaderReferenced(headerTableLength() - 1);
- assertHeaderReferenced(headerTableLength() - hpackReader.headerCount);
- }
-
- @Test public void huffmanDecodingSupported() throws IOException {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x04); // == Literal indexed ==
- // Indexed name (idx = 4) -> :path
- out.writeByte(0x8b); // Literal value Huffman encoded 11 bytes
- // decodes to www.example.com which is length 15
- byte[] huffmanBytes = new byte[] {
- (byte) 0xdb, (byte) 0x6d, (byte) 0x88, (byte) 0x3e,
- (byte) 0x68, (byte) 0xd1, (byte) 0xcb, (byte) 0x12,
- (byte) 0x25, (byte) 0xba, (byte) 0x7f};
- out.write(huffmanBytes, 0, huffmanBytes.length);
-
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(1, hpackReader.headerCount);
- assertEquals(52, hpackReader.headerTableByteCount);
-
- Header entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":path", "www.example.com", 52);
- assertHeaderReferenced(headerTableLength() - 1);
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.1
- */
- @Test public void readLiteralHeaderFieldWithIndexing() throws IOException {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-key");
-
- out.writeByte(0x0d); // Literal value (len = 13)
- out.writeUtf8("custom-header");
-
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(1, hpackReader.headerCount);
- assertEquals(55, hpackReader.headerTableByteCount);
-
- Header entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, "custom-key", "custom-header", 55);
- assertHeaderReferenced(headerTableLength() - 1);
-
- assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndReset());
- }
-
- /**
- * Literal Header Field without Indexing - New Name
- */
- @Test public void literalHeaderFieldWithoutIndexingNewName() throws IOException {
- List<Header> headerBlock = headerEntries("custom-key", "custom-header");
-
- OkBuffer expectedBytes = new OkBuffer();
-
- expectedBytes.writeByte(0x40); // Not indexed
- expectedBytes.writeByte(0x0a); // Literal name (len = 10)
- expectedBytes.write("custom-key".getBytes(), 0, 10);
-
- expectedBytes.writeByte(0x0d); // Literal value (len = 13)
- expectedBytes.write("custom-header".getBytes(), 0, 13);
-
- hpackWriter.writeHeaders(headerBlock);
- assertEquals(expectedBytes, bytesOut);
-
- bytesIn.write(bytesOut, bytesOut.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(0, hpackReader.headerCount);
-
- assertEquals(headerBlock, hpackReader.getAndReset());
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.2
- */
- @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException {
- List<Header> headerBlock = headerEntries(":path", "/sample/path");
-
- OkBuffer expectedBytes = new OkBuffer();
- expectedBytes.writeByte(0x44); // == Literal not indexed ==
- // Indexed name (idx = 4) -> :path
- expectedBytes.writeByte(0x0c); // Literal value (len = 12)
- expectedBytes.write("/sample/path".getBytes(), 0, 12);
-
- hpackWriter.writeHeaders(headerBlock);
- assertEquals(expectedBytes, bytesOut);
-
- bytesIn.write(bytesOut, bytesOut.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(0, hpackReader.headerCount);
-
- assertEquals(headerBlock, hpackReader.getAndReset());
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.3
- */
- @Test public void readIndexedHeaderField() throws IOException {
- bytesIn.writeByte(0x82); // == Indexed - Add ==
- // idx = 2 -> :method: GET
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(1, hpackReader.headerCount);
- assertEquals(42, hpackReader.headerTableByteCount);
-
- Header entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.2.1
- */
- @Test public void toggleIndex() throws IOException {
- // Static table entries are copied to the top of the reference set.
- bytesIn.writeByte(0x82); // == Indexed - Add ==
- // idx = 2 -> :method: GET
- // Specifying an index to an entry in the reference set removes it.
- bytesIn.writeByte(0x81); // == Indexed - Remove ==
- // idx = 1 -> :method: GET
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- assertEquals(1, hpackReader.headerCount);
- assertEquals(42, hpackReader.headerTableByteCount);
-
- Header entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderNotReferenced(headerTableLength() - 1);
-
- assertTrue(hpackReader.getAndReset().isEmpty());
- }
-
- /** Ensure a later toggle of the same index emits! */
- @Test public void toggleIndexOffOn() throws IOException {
-
- bytesIn.writeByte(0x82); // Copy static header 1 to the header table as index 1.
- bytesIn.writeByte(0x81); // Remove index 1 from the reference set.
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- assertEquals(1, hpackReader.headerCount);
- assertTrue(hpackReader.getAndReset().isEmpty());
-
- bytesIn.writeByte(0x81); // Add index 1 back to the reference set.
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- assertEquals(1, hpackReader.headerCount);
- assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
- }
-
- /** Check later toggle of the same index for large header sets. */
- @Test public void toggleIndexOffBeyond64Entries() throws IOException {
- int expectedHeaderCount = 65;
-
- for (int i = 0; i < expectedHeaderCount; i++) {
- bytesIn.writeByte(0x82 + i); // Copy static header 1 to the header table as index 1.
- bytesIn.writeByte(0x81); // Remove index 1 from the reference set.
- }
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- assertEquals(expectedHeaderCount, hpackReader.headerCount);
- assertTrue(hpackReader.getAndReset().isEmpty());
-
- bytesIn.writeByte(0x81); // Add index 1 back to the reference set.
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- assertEquals(expectedHeaderCount, hpackReader.headerCount);
- assertHeaderReferenced(headerTableLength() - expectedHeaderCount);
- assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.1.4
- */
- @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException {
- bytesIn.writeByte(0x82); // == Indexed - Add ==
- // idx = 2 -> :method: GET
-
- hpackReader.maxHeaderTableByteCount(0); // SETTINGS_HEADER_TABLE_SIZE == 0
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
-
- // Not buffered in header table.
- assertEquals(0, hpackReader.headerCount);
-
- assertEquals(headerEntries(":method", "GET"), hpackReader.getAndReset());
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.2
- */
- @Test public void readRequestExamplesWithoutHuffman() throws IOException {
- OkBuffer out = firstRequestWithoutHuffman();
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- checkReadFirstRequestWithoutHuffman();
-
- out = secondRequestWithoutHuffman();
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- checkReadSecondRequestWithoutHuffman();
-
- out = thirdRequestWithoutHuffman();
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- checkReadThirdRequestWithoutHuffman();
- }
-
- private OkBuffer firstRequestWithoutHuffman() {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x82); // == Indexed - Add ==
- // idx = 2 -> :method: GET
- out.writeByte(0x87); // == Indexed - Add ==
- // idx = 7 -> :scheme: http
- out.writeByte(0x86); // == Indexed - Add ==
- // idx = 6 -> :path: /
- out.writeByte(0x04); // == Literal indexed ==
- // Indexed name (idx = 4) -> :authority
- out.writeByte(0x0f); // Literal value (len = 15)
- out.writeUtf8("www.example.com");
-
- return out;
- }
-
- private void checkReadFirstRequestWithoutHuffman() {
- assertEquals(4, hpackReader.headerCount);
-
- // [ 1] (s = 57) :authority: www.example.com
- Header entry = hpackReader.headerTable[headerTableLength() - 4];
- checkEntry(entry, ":authority", "www.example.com", 57);
- assertHeaderReferenced(headerTableLength() - 4);
-
- // [ 2] (s = 38) :path: /
- entry = hpackReader.headerTable[headerTableLength() - 3];
- checkEntry(entry, ":path", "/", 38);
- assertHeaderReferenced(headerTableLength() - 3);
-
- // [ 3] (s = 43) :scheme: http
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, ":scheme", "http", 43);
- assertHeaderReferenced(headerTableLength() - 2);
-
- // [ 4] (s = 42) :method: GET
- entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- // Table size: 180
- assertEquals(180, hpackReader.headerTableByteCount);
-
- // Decoded header set:
- assertEquals(headerEntries(
- ":method", "GET",
- ":scheme", "http",
- ":path", "/",
- ":authority", "www.example.com"), hpackReader.getAndReset());
- }
-
- private OkBuffer secondRequestWithoutHuffman() {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x1b); // == Literal indexed ==
- // Indexed name (idx = 27) -> cache-control
- out.writeByte(0x08); // Literal value (len = 8)
- out.writeUtf8("no-cache");
-
- return out;
- }
-
- private void checkReadSecondRequestWithoutHuffman() {
- assertEquals(5, hpackReader.headerCount);
-
- // [ 1] (s = 53) cache-control: no-cache
- Header entry = hpackReader.headerTable[headerTableLength() - 5];
- checkEntry(entry, "cache-control", "no-cache", 53);
- assertHeaderReferenced(headerTableLength() - 5);
-
- // [ 2] (s = 57) :authority: www.example.com
- entry = hpackReader.headerTable[headerTableLength() - 4];
- checkEntry(entry, ":authority", "www.example.com", 57);
- assertHeaderReferenced(headerTableLength() - 4);
-
- // [ 3] (s = 38) :path: /
- entry = hpackReader.headerTable[headerTableLength() - 3];
- checkEntry(entry, ":path", "/", 38);
- assertHeaderReferenced(headerTableLength() - 3);
-
- // [ 4] (s = 43) :scheme: http
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, ":scheme", "http", 43);
- assertHeaderReferenced(headerTableLength() - 2);
-
- // [ 5] (s = 42) :method: GET
- entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- // Table size: 233
- assertEquals(233, hpackReader.headerTableByteCount);
-
- // Decoded header set:
- assertEquals(headerEntries(
- ":method", "GET",
- ":scheme", "http",
- ":path", "/",
- ":authority", "www.example.com",
- "cache-control", "no-cache"), hpackReader.getAndReset());
- }
-
- private OkBuffer thirdRequestWithoutHuffman() {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x80); // == Empty reference set ==
- out.writeByte(0x85); // == Indexed - Add ==
- // idx = 5 -> :method: GET
- out.writeByte(0x8c); // == Indexed - Add ==
- // idx = 12 -> :scheme: https
- out.writeByte(0x8b); // == Indexed - Add ==
- // idx = 11 -> :path: /index.html
- out.writeByte(0x84); // == Indexed - Add ==
- // idx = 4 -> :authority: www.example.com
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x0a); // Literal name (len = 10)
- out.writeUtf8("custom-key");
- out.writeByte(0x0c); // Literal value (len = 12)
- out.writeUtf8("custom-value");
-
- return out;
- }
-
- private void checkReadThirdRequestWithoutHuffman() {
- assertEquals(8, hpackReader.headerCount);
-
- // [ 1] (s = 54) custom-key: custom-value
- Header entry = hpackReader.headerTable[headerTableLength() - 8];
- checkEntry(entry, "custom-key", "custom-value", 54);
- assertHeaderReferenced(headerTableLength() - 8);
-
- // [ 2] (s = 48) :path: /index.html
- entry = hpackReader.headerTable[headerTableLength() - 7];
- checkEntry(entry, ":path", "/index.html", 48);
- assertHeaderReferenced(headerTableLength() - 7);
-
- // [ 3] (s = 44) :scheme: https
- entry = hpackReader.headerTable[headerTableLength() - 6];
- checkEntry(entry, ":scheme", "https", 44);
- assertHeaderReferenced(headerTableLength() - 6);
-
- // [ 4] (s = 53) cache-control: no-cache
- entry = hpackReader.headerTable[headerTableLength() - 5];
- checkEntry(entry, "cache-control", "no-cache", 53);
- assertHeaderNotReferenced(headerTableLength() - 5);
-
- // [ 5] (s = 57) :authority: www.example.com
- entry = hpackReader.headerTable[headerTableLength() - 4];
- checkEntry(entry, ":authority", "www.example.com", 57);
- assertHeaderReferenced(headerTableLength() - 4);
-
- // [ 6] (s = 38) :path: /
- entry = hpackReader.headerTable[headerTableLength() - 3];
- checkEntry(entry, ":path", "/", 38);
- assertHeaderNotReferenced(headerTableLength() - 3);
-
- // [ 7] (s = 43) :scheme: http
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, ":scheme", "http", 43);
- assertHeaderNotReferenced(headerTableLength() - 2);
-
- // [ 8] (s = 42) :method: GET
- entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- // Table size: 379
- assertEquals(379, hpackReader.headerTableByteCount);
-
- // Decoded header set:
- // TODO: order is not correct per docs, but then again, the spec doesn't require ordering.
- assertEquals(headerEntries(
- ":method", "GET",
- ":authority", "www.example.com",
- ":scheme", "https",
- ":path", "/index.html",
- "custom-key", "custom-value"), hpackReader.getAndReset());
- }
-
- /**
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-E.3
- */
- @Test public void readRequestExamplesWithHuffman() throws IOException {
- OkBuffer out = firstRequestWithHuffman();
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- checkReadFirstRequestWithHuffman();
-
- out = secondRequestWithHuffman();
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- checkReadSecondRequestWithHuffman();
-
- out = thirdRequestWithHuffman();
- bytesIn.write(out, out.size());
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- checkReadThirdRequestWithHuffman();
- }
-
- private OkBuffer firstRequestWithHuffman() {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x82); // == Indexed - Add ==
- // idx = 2 -> :method: GET
- out.writeByte(0x87); // == Indexed - Add ==
- // idx = 7 -> :scheme: http
- out.writeByte(0x86); // == Indexed - Add ==
- // idx = 6 -> :path: /
- out.writeByte(0x04); // == Literal indexed ==
- // Indexed name (idx = 4) -> :authority
- out.writeByte(0x8b); // Literal value Huffman encoded 11 bytes
- // decodes to www.example.com which is length 15
- byte[] huffmanBytes = new byte[] {
- (byte) 0xdb, (byte) 0x6d, (byte) 0x88, (byte) 0x3e,
- (byte) 0x68, (byte) 0xd1, (byte) 0xcb, (byte) 0x12,
- (byte) 0x25, (byte) 0xba, (byte) 0x7f};
- out.write(huffmanBytes, 0, huffmanBytes.length);
-
- return out;
- }
-
- private void checkReadFirstRequestWithHuffman() {
- assertEquals(4, hpackReader.headerCount);
-
- // [ 1] (s = 57) :authority: www.example.com
- Header entry = hpackReader.headerTable[headerTableLength() - 4];
- checkEntry(entry, ":authority", "www.example.com", 57);
- assertHeaderReferenced(headerTableLength() - 4);
-
- // [ 2] (s = 38) :path: /
- entry = hpackReader.headerTable[headerTableLength() - 3];
- checkEntry(entry, ":path", "/", 38);
- assertHeaderReferenced(headerTableLength() - 3);
-
- // [ 3] (s = 43) :scheme: http
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, ":scheme", "http", 43);
- assertHeaderReferenced(headerTableLength() - 2);
-
- // [ 4] (s = 42) :method: GET
- entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- // Table size: 180
- assertEquals(180, hpackReader.headerTableByteCount);
-
- // Decoded header set:
- assertEquals(headerEntries(
- ":method", "GET",
- ":scheme", "http",
- ":path", "/",
- ":authority", "www.example.com"), hpackReader.getAndReset());
- }
-
- private OkBuffer secondRequestWithHuffman() {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x1b); // == Literal indexed ==
- // Indexed name (idx = 27) -> cache-control
- out.writeByte(0x86); // Literal value Huffman encoded 6 bytes
- // decodes to no-cache which is length 8
- byte[] huffmanBytes = new byte[] {
- (byte) 0x63, (byte) 0x65, (byte) 0x4a, (byte) 0x13,
- (byte) 0x98, (byte) 0xff};
- out.write(huffmanBytes, 0, huffmanBytes.length);
-
- return out;
- }
-
- private void checkReadSecondRequestWithHuffman() {
- assertEquals(5, hpackReader.headerCount);
-
- // [ 1] (s = 53) cache-control: no-cache
- Header entry = hpackReader.headerTable[headerTableLength() - 5];
- checkEntry(entry, "cache-control", "no-cache", 53);
- assertHeaderReferenced(headerTableLength() - 5);
-
- // [ 2] (s = 57) :authority: www.example.com
- entry = hpackReader.headerTable[headerTableLength() - 4];
- checkEntry(entry, ":authority", "www.example.com", 57);
- assertHeaderReferenced(headerTableLength() - 4);
-
- // [ 3] (s = 38) :path: /
- entry = hpackReader.headerTable[headerTableLength() - 3];
- checkEntry(entry, ":path", "/", 38);
- assertHeaderReferenced(headerTableLength() - 3);
-
- // [ 4] (s = 43) :scheme: http
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, ":scheme", "http", 43);
- assertHeaderReferenced(headerTableLength() - 2);
-
- // [ 5] (s = 42) :method: GET
- entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- // Table size: 233
- assertEquals(233, hpackReader.headerTableByteCount);
-
- // Decoded header set:
- assertEquals(headerEntries(
- ":method", "GET",
- ":scheme", "http",
- ":path", "/",
- ":authority", "www.example.com",
- "cache-control", "no-cache"), hpackReader.getAndReset());
- }
-
- private OkBuffer thirdRequestWithHuffman() {
- OkBuffer out = new OkBuffer();
-
- out.writeByte(0x80); // == Empty reference set ==
- out.writeByte(0x85); // == Indexed - Add ==
- // idx = 5 -> :method: GET
- out.writeByte(0x8c); // == Indexed - Add ==
- // idx = 12 -> :scheme: https
- out.writeByte(0x8b); // == Indexed - Add ==
- // idx = 11 -> :path: /index.html
- out.writeByte(0x84); // == Indexed - Add ==
- // idx = 4 -> :authority: www.example.com
- out.writeByte(0x00); // Literal indexed
- out.writeByte(0x88); // Literal name Huffman encoded 8 bytes
- // decodes to custom-key which is length 10
- byte[] huffmanBytes = new byte[] {
- (byte) 0x4e, (byte) 0xb0, (byte) 0x8b, (byte) 0x74,
- (byte) 0x97, (byte) 0x90, (byte) 0xfa, (byte) 0x7f};
- out.write(huffmanBytes, 0, huffmanBytes.length);
- out.writeByte(0x89); // Literal value Huffman encoded 6 bytes
- // decodes to custom-value which is length 12
- huffmanBytes = new byte[] {
- (byte) 0x4e, (byte) 0xb0, (byte) 0x8b, (byte) 0x74,
- (byte) 0x97, (byte) 0x9a, (byte) 0x17, (byte) 0xa8,
- (byte) 0xff};
- out.write(huffmanBytes, 0, huffmanBytes.length);
-
- return out;
- }
-
- private void checkReadThirdRequestWithHuffman() {
- assertEquals(8, hpackReader.headerCount);
-
- // [ 1] (s = 54) custom-key: custom-value
- Header entry = hpackReader.headerTable[headerTableLength() - 8];
- checkEntry(entry, "custom-key", "custom-value", 54);
- assertHeaderReferenced(headerTableLength() - 8);
-
- // [ 2] (s = 48) :path: /index.html
- entry = hpackReader.headerTable[headerTableLength() - 7];
- checkEntry(entry, ":path", "/index.html", 48);
- assertHeaderReferenced(headerTableLength() - 7);
-
- // [ 3] (s = 44) :scheme: https
- entry = hpackReader.headerTable[headerTableLength() - 6];
- checkEntry(entry, ":scheme", "https", 44);
- assertHeaderReferenced(headerTableLength() - 6);
-
- // [ 4] (s = 53) cache-control: no-cache
- entry = hpackReader.headerTable[headerTableLength() - 5];
- checkEntry(entry, "cache-control", "no-cache", 53);
- assertHeaderNotReferenced(headerTableLength() - 5);
-
- // [ 5] (s = 57) :authority: www.example.com
- entry = hpackReader.headerTable[headerTableLength() - 4];
- checkEntry(entry, ":authority", "www.example.com", 57);
- assertHeaderReferenced(headerTableLength() - 4);
-
- // [ 6] (s = 38) :path: /
- entry = hpackReader.headerTable[headerTableLength() - 3];
- checkEntry(entry, ":path", "/", 38);
- assertHeaderNotReferenced(headerTableLength() - 3);
-
- // [ 7] (s = 43) :scheme: http
- entry = hpackReader.headerTable[headerTableLength() - 2];
- checkEntry(entry, ":scheme", "http", 43);
- assertHeaderNotReferenced(headerTableLength() - 2);
-
- // [ 8] (s = 42) :method: GET
- entry = hpackReader.headerTable[headerTableLength() - 1];
- checkEntry(entry, ":method", "GET", 42);
- assertHeaderReferenced(headerTableLength() - 1);
-
- // Table size: 379
- assertEquals(379, hpackReader.headerTableByteCount);
-
- // Decoded header set:
- // TODO: order is not correct per docs, but then again, the spec doesn't require ordering.
- assertEquals(headerEntries(
- ":method", "GET",
- ":authority", "www.example.com",
- ":scheme", "https",
- ":path", "/index.html",
- "custom-key", "custom-value"), hpackReader.getAndReset());
- }
-
- @Test public void readSingleByteInt() throws IOException {
- assertEquals(10, newReader(byteStream()).readInt(10, 31));
- assertEquals(10, newReader(byteStream()).readInt(0xe0 | 10, 31));
- }
-
- @Test public void readMultibyteInt() throws IOException {
- assertEquals(1337, newReader(byteStream(154, 10)).readInt(31, 31));
- }
-
- @Test public void writeSingleByteInt() throws IOException {
- hpackWriter.writeInt(10, 31, 0);
- assertBytes(10);
- hpackWriter.writeInt(10, 31, 0xe0);
- assertBytes(0xe0 | 10);
- }
-
- @Test public void writeMultibyteInt() throws IOException {
- hpackWriter.writeInt(1337, 31, 0);
- assertBytes(31, 154, 10);
- hpackWriter.writeInt(1337, 31, 0xe0);
- assertBytes(0xe0 | 31, 154, 10);
- }
-
- @Test public void max31BitValue() throws IOException {
- hpackWriter.writeInt(0x7fffffff, 31, 0);
- assertBytes(31, 224, 255, 255, 255, 7);
- assertEquals(0x7fffffff,
- newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31));
- }
-
- @Test public void prefixMask() throws IOException {
- hpackWriter.writeInt(31, 31, 0);
- assertBytes(31, 0);
- assertEquals(31, newReader(byteStream(0)).readInt(31, 31));
- }
-
- @Test public void prefixMaskMinusOne() throws IOException {
- hpackWriter.writeInt(30, 31, 0);
- assertBytes(30);
- assertEquals(31, newReader(byteStream(0)).readInt(31, 31));
- }
-
- @Test public void zero() throws IOException {
- hpackWriter.writeInt(0, 31, 0);
- assertBytes(0);
- assertEquals(0, newReader(byteStream()).readInt(0, 31));
- }
-
- @Test public void headerName() throws IOException {
- hpackWriter.writeByteString(ByteString.encodeUtf8("foo"));
- assertBytes(3, 'f', 'o', 'o');
- assertEquals("foo", newReader(byteStream(3, 'F', 'o', 'o')).readByteString(true).utf8());
- }
-
- @Test public void emptyHeaderName() throws IOException {
- hpackWriter.writeByteString(ByteString.encodeUtf8(""));
- assertBytes(0);
- assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString(true));
- assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString(false));
- }
-
- private HpackDraft05.Reader newReader(OkBuffer source) {
- return new HpackDraft05.Reader(false, 4096, source);
- }
-
- private OkBuffer byteStream(int... bytes) {
- return new OkBuffer().write(intArrayToByteArray(bytes));
- }
-
- private void checkEntry(Header entry, String name, String value, int size) {
- assertEquals(name, entry.name.utf8());
- assertEquals(value, entry.value.utf8());
- assertEquals(size, entry.hpackSize);
- }
-
- private void assertBytes(int... bytes) {
- ByteString expected = intArrayToByteArray(bytes);
- ByteString actual = bytesOut.readByteString(bytesOut.size());
- assertEquals(expected, actual);
- }
-
- private ByteString intArrayToByteArray(int[] bytes) {
- byte[] data = new byte[bytes.length];
- for (int i = 0; i < bytes.length; i++) {
- data[i] = (byte) bytes[i];
- }
- return ByteString.of(data);
- }
-
- private void assertHeaderReferenced(int index) {
- assertTrue(hpackReader.referencedHeaders.get(index));
- }
-
- private void assertHeaderNotReferenced(int index) {
- assertFalse(hpackReader.referencedHeaders.get(index));
- }
-
- private int headerTableLength() {
- return hpackReader.headerTable.length;
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft10Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft10Test.java
new file mode 100644
index 0000000..b886a43
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HpackDraft10Test.java
@@ -0,0 +1,726 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import okio.Buffer;
+import okio.ByteString;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.squareup.okhttp.TestUtil.headerEntries;
+import static okio.ByteString.decodeHex;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class HpackDraft10Test {
+
+ private final Buffer bytesIn = new Buffer();
+ private HpackDraft10.Reader hpackReader;
+ private Buffer bytesOut = new Buffer();
+ private HpackDraft10.Writer hpackWriter;
+
+ @Before public void reset() {
+ hpackReader = newReader(bytesIn);
+ hpackWriter = new HpackDraft10.Writer(bytesOut);
+ }
+
+ /**
+ * Variable-length quantity special cases strings which are longer than 127
+ * bytes. Values such as cookies can be 4KiB, and should be possible to send.
+ *
+ * <p> http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-5.2
+ */
+ @Test public void largeHeaderValue() throws IOException {
+ char[] value = new char[4096];
+ Arrays.fill(value, '!');
+ List<Header> headerBlock = headerEntries("cookie", new String(value));
+
+ hpackWriter.writeHeaders(headerBlock);
+ bytesIn.writeAll(bytesOut);
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerBlock, hpackReader.getAndResetHeaderList());
+ }
+
+ /**
+ * HPACK has a max header table size, which can be smaller than the max header message.
+ * Ensure the larger header content is not lost.
+ */
+ @Test public void tooLargeToHPackIsStillEmitted() throws IOException {
+ bytesIn.writeByte(0x00); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-key");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ hpackReader.headerTableSizeSetting(1);
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList());
+ }
+
+ /** Oldest entries are evicted to support newer ones. */
+ @Test public void testEviction() throws IOException {
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-foo");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-bar");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-baz");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ // Set to only support 110 bytes (enough for 2 headers).
+ hpackReader.headerTableSizeSetting(110);
+ hpackReader.readHeaders();
+
+ assertEquals(2, hpackReader.headerCount);
+
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, "custom-bar", "custom-header", 55);
+
+ entry = hpackReader.dynamicTable[headerTableLength() - 2];
+ checkEntry(entry, "custom-baz", "custom-header", 55);
+
+ // Once a header field is decoded and added to the reconstructed header
+ // list, it cannot be removed from it. Hence, foo is here.
+ assertEquals(
+ headerEntries(
+ "custom-foo", "custom-header",
+ "custom-bar", "custom-header",
+ "custom-baz", "custom-header"),
+ hpackReader.getAndResetHeaderList());
+
+ // Simulate receiving a small settings frame, that implies eviction.
+ hpackReader.headerTableSizeSetting(55);
+ assertEquals(1, hpackReader.headerCount);
+ }
+
+ /** Header table backing array is initially 8 long, let's ensure it grows. */
+ @Test public void dynamicallyGrowsBeyond64Entries() throws IOException {
+ for (int i = 0; i < 256; i++) {
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-foo");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+ }
+
+ hpackReader.headerTableSizeSetting(16384); // Lots of headers need more room!
+ hpackReader.readHeaders();
+
+ assertEquals(256, hpackReader.headerCount);
+ }
+
+ @Test public void huffmanDecodingSupported() throws IOException {
+ bytesIn.writeByte(0x44); // == Literal indexed ==
+ // Indexed name (idx = 4) -> :path
+ bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes
+ // decodes to www.example.com which is length 15
+ bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff"));
+
+ hpackReader.readHeaders();
+
+ assertEquals(1, hpackReader.headerCount);
+ assertEquals(52, hpackReader.dynamicTableByteCount);
+
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":path", "www.example.com", 52);
+ }
+
+ /**
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-C.2.1
+ */
+ @Test public void readLiteralHeaderFieldWithIndexing() throws IOException {
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-key");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ hpackReader.readHeaders();
+
+ assertEquals(1, hpackReader.headerCount);
+ assertEquals(55, hpackReader.dynamicTableByteCount);
+
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, "custom-key", "custom-header", 55);
+
+ assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList());
+ }
+
+ /**
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-C.2.2
+ */
+ @Test public void literalHeaderFieldWithoutIndexingIndexedName() throws IOException {
+ List<Header> headerBlock = headerEntries(":path", "/sample/path");
+
+ bytesIn.writeByte(0x04); // == Literal not indexed ==
+ // Indexed name (idx = 4) -> :path
+ bytesIn.writeByte(0x0c); // Literal value (len = 12)
+ bytesIn.writeUtf8("/sample/path");
+
+ hpackWriter.writeHeaders(headerBlock);
+ assertEquals(bytesIn, bytesOut);
+
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerBlock, hpackReader.getAndResetHeaderList());
+ }
+
+ @Test public void literalHeaderFieldWithoutIndexingNewName() throws IOException {
+ List<Header> headerBlock = headerEntries("custom-key", "custom-header");
+
+ bytesIn.writeByte(0x00); // Not indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-key");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ hpackWriter.writeHeaders(headerBlock);
+ assertEquals(bytesIn, bytesOut);
+
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerBlock, hpackReader.getAndResetHeaderList());
+ }
+
+ @Test public void literalHeaderFieldNeverIndexedIndexedName() throws IOException {
+ bytesIn.writeByte(0x14); // == Literal never indexed ==
+ // Indexed name (idx = 4) -> :path
+ bytesIn.writeByte(0x0c); // Literal value (len = 12)
+ bytesIn.writeUtf8("/sample/path");
+
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerEntries(":path", "/sample/path"), hpackReader.getAndResetHeaderList());
+ }
+
+ @Test public void literalHeaderFieldNeverIndexedNewName() throws IOException {
+ bytesIn.writeByte(0x10); // Never indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-key");
+
+ bytesIn.writeByte(0x0d); // Literal value (len = 13)
+ bytesIn.writeUtf8("custom-header");
+
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerEntries("custom-key", "custom-header"), hpackReader.getAndResetHeaderList());
+ }
+
+ @Test public void staticHeaderIsNotCopiedIntoTheIndexedTable() throws IOException {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.headerCount);
+ assertEquals(0, hpackReader.dynamicTableByteCount);
+
+ assertEquals(null, hpackReader.dynamicTable[headerTableLength() - 1]);
+
+ assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList());
+ }
+
+ // Example taken from twitter/hpack DecoderTest.testUnusedIndex
+ @Test public void readIndexedHeaderFieldIndex0() throws IOException {
+ bytesIn.writeByte(0x80); // == Indexed - Add idx = 0
+
+ try {
+ hpackReader.readHeaders();
+ fail();
+ } catch (IOException e) {
+ assertEquals("index == 0", e.getMessage());
+ }
+ }
+
+ // Example taken from twitter/hpack DecoderTest.testIllegalIndex
+ @Test public void readIndexedHeaderFieldTooLargeIndex() throws IOException {
+ bytesIn.writeShort(0xff00); // == Indexed - Add idx = 127
+
+ try {
+ hpackReader.readHeaders();
+ fail();
+ } catch (IOException e) {
+ assertEquals("Header index too large 127", e.getMessage());
+ }
+ }
+
+ // Example taken from twitter/hpack DecoderTest.testInsidiousIndex
+ @Test public void readIndexedHeaderFieldInsidiousIndex() throws IOException {
+ bytesIn.writeByte(0xff); // == Indexed - Add ==
+ bytesIn.write(decodeHex("8080808008")); // idx = -2147483521
+
+ try {
+ hpackReader.readHeaders();
+ fail();
+ } catch (IOException e) {
+ assertEquals("Header index too large -2147483521", e.getMessage());
+ }
+ }
+
+ // Example taken from twitter/hpack DecoderTest.testHeaderTableSizeUpdate
+ @Test public void minMaxHeaderTableSize() throws IOException {
+ bytesIn.writeByte(0x20);
+ hpackReader.readHeaders();
+
+ assertEquals(0, hpackReader.maxDynamicTableByteCount());
+
+ bytesIn.writeByte(0x3f); // encode size 4096
+ bytesIn.writeByte(0xe1);
+ bytesIn.writeByte(0x1f);
+ hpackReader.readHeaders();
+
+ assertEquals(4096, hpackReader.maxDynamicTableByteCount());
+ }
+
+ // Example taken from twitter/hpack DecoderTest.testIllegalHeaderTableSizeUpdate
+ @Test public void cannotSetTableSizeLargerThanSettingsValue() throws IOException {
+ bytesIn.writeByte(0x3f); // encode size 4097
+ bytesIn.writeByte(0xe2);
+ bytesIn.writeByte(0x1f);
+
+ try {
+ hpackReader.readHeaders();
+ fail();
+ } catch (IOException e) {
+ assertEquals("Invalid dynamic table size update 4097", e.getMessage());
+ }
+ }
+
+ // Example taken from twitter/hpack DecoderTest.testInsidiousMaxHeaderSize
+ @Test public void readHeaderTableStateChangeInsidiousMaxHeaderByteCount() throws IOException {
+ bytesIn.writeByte(0x3f);
+ bytesIn.write(decodeHex("e1ffffff07")); // count = -2147483648
+
+ try {
+ hpackReader.readHeaders();
+ fail();
+ } catch (IOException e) {
+ assertEquals("Invalid dynamic table size update -2147483648", e.getMessage());
+ }
+ }
+
+ /**
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-C.2.4
+ */
+ @Test public void readIndexedHeaderFieldFromStaticTableWithoutBuffering() throws IOException {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+
+ hpackReader.headerTableSizeSetting(0); // SETTINGS_HEADER_TABLE_SIZE == 0
+ hpackReader.readHeaders();
+
+ // Not buffered in header table.
+ assertEquals(0, hpackReader.headerCount);
+
+ assertEquals(headerEntries(":method", "GET"), hpackReader.getAndResetHeaderList());
+ }
+
+ /**
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-C.2
+ */
+ @Test public void readRequestExamplesWithoutHuffman() throws IOException {
+ firstRequestWithoutHuffman();
+ hpackReader.readHeaders();
+ checkReadFirstRequestWithoutHuffman();
+
+ secondRequestWithoutHuffman();
+ hpackReader.readHeaders();
+ checkReadSecondRequestWithoutHuffman();
+
+ thirdRequestWithoutHuffman();
+ hpackReader.readHeaders();
+ checkReadThirdRequestWithoutHuffman();
+ }
+
+ private void firstRequestWithoutHuffman() {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+ bytesIn.writeByte(0x86); // == Indexed - Add ==
+ // idx = 7 -> :scheme: http
+ bytesIn.writeByte(0x84); // == Indexed - Add ==
+ // idx = 6 -> :path: /
+ bytesIn.writeByte(0x41); // == Literal indexed ==
+ // Indexed name (idx = 4) -> :authority
+ bytesIn.writeByte(0x0f); // Literal value (len = 15)
+ bytesIn.writeUtf8("www.example.com");
+ }
+
+ private void checkReadFirstRequestWithoutHuffman() {
+ assertEquals(1, hpackReader.headerCount);
+
+ // [ 1] (s = 57) :authority: www.example.com
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":authority", "www.example.com", 57);
+
+ // Table size: 57
+ assertEquals(57, hpackReader.dynamicTableByteCount);
+
+ // Decoded header list:
+ assertEquals(headerEntries(
+ ":method", "GET",
+ ":scheme", "http",
+ ":path", "/",
+ ":authority", "www.example.com"), hpackReader.getAndResetHeaderList());
+ }
+
+ private void secondRequestWithoutHuffman() {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+ bytesIn.writeByte(0x86); // == Indexed - Add ==
+ // idx = 7 -> :scheme: http
+ bytesIn.writeByte(0x84); // == Indexed - Add ==
+ // idx = 6 -> :path: /
+ bytesIn.writeByte(0xbe); // == Indexed - Add ==
+ // Indexed name (idx = 62) -> :authority: www.example.com
+ bytesIn.writeByte(0x58); // == Literal indexed ==
+ // Indexed name (idx = 24) -> cache-control
+ bytesIn.writeByte(0x08); // Literal value (len = 8)
+ bytesIn.writeUtf8("no-cache");
+ }
+
+ private void checkReadSecondRequestWithoutHuffman() {
+ assertEquals(2, hpackReader.headerCount);
+
+ // [ 1] (s = 53) cache-control: no-cache
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 2];
+ checkEntry(entry, "cache-control", "no-cache", 53);
+
+ // [ 2] (s = 57) :authority: www.example.com
+ entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":authority", "www.example.com", 57);
+
+ // Table size: 110
+ assertEquals(110, hpackReader.dynamicTableByteCount);
+
+ // Decoded header list:
+ assertEquals(headerEntries(
+ ":method", "GET",
+ ":scheme", "http",
+ ":path", "/",
+ ":authority", "www.example.com",
+ "cache-control", "no-cache"), hpackReader.getAndResetHeaderList());
+ }
+
+ private void thirdRequestWithoutHuffman() {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+ bytesIn.writeByte(0x87); // == Indexed - Add ==
+ // idx = 7 -> :scheme: http
+ bytesIn.writeByte(0x85); // == Indexed - Add ==
+ // idx = 5 -> :path: /index.html
+ bytesIn.writeByte(0xbf); // == Indexed - Add ==
+ // Indexed name (idx = 63) -> :authority: www.example.com
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x0a); // Literal name (len = 10)
+ bytesIn.writeUtf8("custom-key");
+ bytesIn.writeByte(0x0c); // Literal value (len = 12)
+ bytesIn.writeUtf8("custom-value");
+ }
+
+ private void checkReadThirdRequestWithoutHuffman() {
+ assertEquals(3, hpackReader.headerCount);
+
+ // [ 1] (s = 54) custom-key: custom-value
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 3];
+ checkEntry(entry, "custom-key", "custom-value", 54);
+
+ // [ 2] (s = 53) cache-control: no-cache
+ entry = hpackReader.dynamicTable[headerTableLength() - 2];
+ checkEntry(entry, "cache-control", "no-cache", 53);
+
+ // [ 3] (s = 57) :authority: www.example.com
+ entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":authority", "www.example.com", 57);
+
+ // Table size: 164
+ assertEquals(164, hpackReader.dynamicTableByteCount);
+
+ // Decoded header list:
+ assertEquals(headerEntries(
+ ":method", "GET",
+ ":scheme", "https",
+ ":path", "/index.html",
+ ":authority", "www.example.com",
+ "custom-key", "custom-value"), hpackReader.getAndResetHeaderList());
+ }
+
+ /**
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-C.4
+ */
+ @Test public void readRequestExamplesWithHuffman() throws IOException {
+ firstRequestWithHuffman();
+ hpackReader.readHeaders();
+ checkReadFirstRequestWithHuffman();
+
+ secondRequestWithHuffman();
+ hpackReader.readHeaders();
+ checkReadSecondRequestWithHuffman();
+
+ thirdRequestWithHuffman();
+ hpackReader.readHeaders();
+ checkReadThirdRequestWithHuffman();
+ }
+
+ private void firstRequestWithHuffman() {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+ bytesIn.writeByte(0x86); // == Indexed - Add ==
+ // idx = 6 -> :scheme: http
+ bytesIn.writeByte(0x84); // == Indexed - Add ==
+ // idx = 4 -> :path: /
+ bytesIn.writeByte(0x41); // == Literal indexed ==
+ // Indexed name (idx = 1) -> :authority
+ bytesIn.writeByte(0x8c); // Literal value Huffman encoded 12 bytes
+ // decodes to www.example.com which is length 15
+ bytesIn.write(decodeHex("f1e3c2e5f23a6ba0ab90f4ff"));
+ }
+
+ private void checkReadFirstRequestWithHuffman() {
+ assertEquals(1, hpackReader.headerCount);
+
+ // [ 1] (s = 57) :authority: www.example.com
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":authority", "www.example.com", 57);
+
+ // Table size: 57
+ assertEquals(57, hpackReader.dynamicTableByteCount);
+
+ // Decoded header list:
+ assertEquals(headerEntries(
+ ":method", "GET",
+ ":scheme", "http",
+ ":path", "/",
+ ":authority", "www.example.com"), hpackReader.getAndResetHeaderList());
+ }
+
+ private void secondRequestWithHuffman() {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+ bytesIn.writeByte(0x86); // == Indexed - Add ==
+ // idx = 6 -> :scheme: http
+ bytesIn.writeByte(0x84); // == Indexed - Add ==
+ // idx = 4 -> :path: /
+ bytesIn.writeByte(0xbe); // == Indexed - Add ==
+ // idx = 62 -> :authority: www.example.com
+ bytesIn.writeByte(0x58); // == Literal indexed ==
+ // Indexed name (idx = 24) -> cache-control
+ bytesIn.writeByte(0x86); // Literal value Huffman encoded 6 bytes
+ // decodes to no-cache which is length 8
+ bytesIn.write(decodeHex("a8eb10649cbf"));
+ }
+
+ private void checkReadSecondRequestWithHuffman() {
+ assertEquals(2, hpackReader.headerCount);
+
+ // [ 1] (s = 53) cache-control: no-cache
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 2];
+ checkEntry(entry, "cache-control", "no-cache", 53);
+
+ // [ 2] (s = 57) :authority: www.example.com
+ entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":authority", "www.example.com", 57);
+
+ // Table size: 110
+ assertEquals(110, hpackReader.dynamicTableByteCount);
+
+ // Decoded header list:
+ assertEquals(headerEntries(
+ ":method", "GET",
+ ":scheme", "http",
+ ":path", "/",
+ ":authority", "www.example.com",
+ "cache-control", "no-cache"), hpackReader.getAndResetHeaderList());
+ }
+
+ private void thirdRequestWithHuffman() {
+ bytesIn.writeByte(0x82); // == Indexed - Add ==
+ // idx = 2 -> :method: GET
+ bytesIn.writeByte(0x87); // == Indexed - Add ==
+ // idx = 7 -> :scheme: https
+ bytesIn.writeByte(0x85); // == Indexed - Add ==
+ // idx = 5 -> :path: /index.html
+ bytesIn.writeByte(0xbf); // == Indexed - Add ==
+ // idx = 63 -> :authority: www.example.com
+ bytesIn.writeByte(0x40); // Literal indexed
+ bytesIn.writeByte(0x88); // Literal name Huffman encoded 8 bytes
+ // decodes to custom-key which is length 10
+ bytesIn.write(decodeHex("25a849e95ba97d7f"));
+ bytesIn.writeByte(0x89); // Literal value Huffman encoded 9 bytes
+ // decodes to custom-value which is length 12
+ bytesIn.write(decodeHex("25a849e95bb8e8b4bf"));
+ }
+
+ private void checkReadThirdRequestWithHuffman() {
+ assertEquals(3, hpackReader.headerCount);
+
+ // [ 1] (s = 54) custom-key: custom-value
+ Header entry = hpackReader.dynamicTable[headerTableLength() - 3];
+ checkEntry(entry, "custom-key", "custom-value", 54);
+
+ // [ 2] (s = 53) cache-control: no-cache
+ entry = hpackReader.dynamicTable[headerTableLength() - 2];
+ checkEntry(entry, "cache-control", "no-cache", 53);
+
+ // [ 3] (s = 57) :authority: www.example.com
+ entry = hpackReader.dynamicTable[headerTableLength() - 1];
+ checkEntry(entry, ":authority", "www.example.com", 57);
+
+ // Table size: 164
+ assertEquals(164, hpackReader.dynamicTableByteCount);
+
+ // Decoded header list:
+ assertEquals(headerEntries(
+ ":method", "GET",
+ ":scheme", "https",
+ ":path", "/index.html",
+ ":authority", "www.example.com",
+ "custom-key", "custom-value"), hpackReader.getAndResetHeaderList());
+ }
+
+ @Test public void readSingleByteInt() throws IOException {
+ assertEquals(10, newReader(byteStream()).readInt(10, 31));
+ assertEquals(10, newReader(byteStream()).readInt(0xe0 | 10, 31));
+ }
+
+ @Test public void readMultibyteInt() throws IOException {
+ assertEquals(1337, newReader(byteStream(154, 10)).readInt(31, 31));
+ }
+
+ @Test public void writeSingleByteInt() throws IOException {
+ hpackWriter.writeInt(10, 31, 0);
+ assertBytes(10);
+ hpackWriter.writeInt(10, 31, 0xe0);
+ assertBytes(0xe0 | 10);
+ }
+
+ @Test public void writeMultibyteInt() throws IOException {
+ hpackWriter.writeInt(1337, 31, 0);
+ assertBytes(31, 154, 10);
+ hpackWriter.writeInt(1337, 31, 0xe0);
+ assertBytes(0xe0 | 31, 154, 10);
+ }
+
+ @Test public void max31BitValue() throws IOException {
+ hpackWriter.writeInt(0x7fffffff, 31, 0);
+ assertBytes(31, 224, 255, 255, 255, 7);
+ assertEquals(0x7fffffff,
+ newReader(byteStream(224, 255, 255, 255, 7)).readInt(31, 31));
+ }
+
+ @Test public void prefixMask() throws IOException {
+ hpackWriter.writeInt(31, 31, 0);
+ assertBytes(31, 0);
+ assertEquals(31, newReader(byteStream(0)).readInt(31, 31));
+ }
+
+ @Test public void prefixMaskMinusOne() throws IOException {
+ hpackWriter.writeInt(30, 31, 0);
+ assertBytes(30);
+ assertEquals(31, newReader(byteStream(0)).readInt(31, 31));
+ }
+
+ @Test public void zero() throws IOException {
+ hpackWriter.writeInt(0, 31, 0);
+ assertBytes(0);
+ assertEquals(0, newReader(byteStream()).readInt(0, 31));
+ }
+
+ @Test public void lowercaseHeaderNameBeforeEmit() throws IOException {
+ hpackWriter.writeHeaders(Arrays.asList(new Header("FoO", "BaR")));
+ assertBytes(0, 3, 'f', 'o', 'o', 3, 'B', 'a', 'R');
+ }
+
+ @Test public void mixedCaseHeaderNameIsMalformed() throws IOException {
+ try {
+ newReader(byteStream(0, 3, 'F', 'o', 'o', 3, 'B', 'a', 'R')).readHeaders();
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR response malformed: mixed case name: Foo", e.getMessage());
+ }
+ }
+
+ @Test public void emptyHeaderName() throws IOException {
+ hpackWriter.writeByteString(ByteString.encodeUtf8(""));
+ assertBytes(0);
+ assertEquals(ByteString.EMPTY, newReader(byteStream(0)).readByteString());
+ }
+
+ private HpackDraft10.Reader newReader(Buffer source) {
+ return new HpackDraft10.Reader(4096, source);
+ }
+
+ private Buffer byteStream(int... bytes) {
+ return new Buffer().write(intArrayToByteArray(bytes));
+ }
+
+ private void checkEntry(Header entry, String name, String value, int size) {
+ assertEquals(name, entry.name.utf8());
+ assertEquals(value, entry.value.utf8());
+ assertEquals(size, entry.hpackSize);
+ }
+
+ private void assertBytes(int... bytes) throws IOException {
+ ByteString expected = intArrayToByteArray(bytes);
+ ByteString actual = bytesOut.readByteString();
+ assertEquals(expected, actual);
+ }
+
+ private ByteString intArrayToByteArray(int[] bytes) {
+ byte[] data = new byte[bytes.length];
+ for (int i = 0; i < bytes.length; i++) {
+ data[i] = (byte) bytes[i];
+ }
+ return ByteString.of(data);
+ }
+
+ private int headerTableLength() {
+ return hpackReader.dynamicTable.length;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java
deleted file mode 100644
index 248ea09..0000000
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft09Test.java
+++ /dev/null
@@ -1,515 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.spdy;
-
-import com.squareup.okhttp.internal.Util;
-import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import okio.BufferedSource;
-import okio.ByteString;
-import okio.OkBuffer;
-import org.junit.Test;
-
-import static com.squareup.okhttp.internal.Util.headerEntries;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public class Http20Draft09Test {
- static final int expectedStreamId = 15;
-
- @Test public void unknownFrameTypeIgnored() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- frame.writeShort(4); // has a 4-byte field
- frame.writeByte(99); // type 99
- frame.writeByte(0); // no flags
- frame.writeInt(expectedStreamId);
- frame.writeInt(111111111); // custom data
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Consume the unknown frame.
- fr.nextFrame(new BaseTestHandler());
- }
-
- @Test public void onlyOneLiteralHeadersFrame() throws IOException {
- final List<Header> sentHeaders = headerEntries("name", "value");
-
- OkBuffer frame = new OkBuffer();
-
- // Write the headers frame, specifying no more frames are expected.
- {
- OkBuffer headerBytes = literalHeaders(sentHeaders);
- frame.writeShort((int) headerBytes.size());
- frame.writeByte(Http20Draft09.TYPE_HEADERS);
- frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_END_STREAM);
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.write(headerBytes, headerBytes.size());
- }
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Consume the headers frame.
- fr.nextFrame(new BaseTestHandler() {
-
- @Override
- public void headers(boolean outFinished, boolean inFinished, int streamId,
- int associatedStreamId, int priority, List<Header> headerBlock,
- HeadersMode headersMode) {
- assertFalse(outFinished);
- assertTrue(inFinished);
- assertEquals(expectedStreamId, streamId);
- assertEquals(-1, associatedStreamId);
- assertEquals(-1, priority);
- assertEquals(sentHeaders, headerBlock);
- assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
- }
- });
- }
-
- @Test public void headersWithPriority() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final List<Header> sentHeaders = headerEntries("name", "value");
-
- { // Write the headers frame, specifying priority flag and value.
- OkBuffer headerBytes = literalHeaders(sentHeaders);
- frame.writeShort((int) (headerBytes.size() + 4));
- frame.writeByte(Http20Draft09.TYPE_HEADERS);
- frame.writeByte(Http20Draft09.FLAG_END_HEADERS | Http20Draft09.FLAG_PRIORITY);
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.writeInt(0); // Highest priority is 0.
- frame.write(headerBytes, headerBytes.size());
- }
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Consume the headers frame.
- fr.nextFrame(new BaseTestHandler() {
-
- @Override
- public void headers(boolean outFinished, boolean inFinished, int streamId,
- int associatedStreamId, int priority, List<Header> nameValueBlock,
- HeadersMode headersMode) {
- assertFalse(outFinished);
- assertFalse(inFinished);
- assertEquals(expectedStreamId, streamId);
- assertEquals(-1, associatedStreamId);
- assertEquals(0, priority);
- assertEquals(sentHeaders, nameValueBlock);
- assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
- }
- });
- }
-
- /** Headers are compressed, then framed. */
- @Test public void headersFrameThenContinuation() throws IOException {
-
- OkBuffer frame = new OkBuffer();
-
- // Decoding the first header will cross frame boundaries.
- OkBuffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
- { // Write the first headers frame.
- frame.writeShort((int) (headerBlock.size() / 2));
- frame.writeByte(Http20Draft09.TYPE_HEADERS);
- frame.writeByte(0); // no flags
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.write(headerBlock, headerBlock.size() / 2);
- }
-
- { // Write the continuation frame, specifying no more frames are expected.
- frame.writeShort((int) headerBlock.size());
- frame.writeByte(Http20Draft09.TYPE_CONTINUATION);
- frame.writeByte(Http20Draft09.FLAG_END_HEADERS);
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.write(headerBlock, headerBlock.size());
- }
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Reading the above frames should result in a concatenated headerBlock.
- fr.nextFrame(new BaseTestHandler() {
-
- @Override
- public void headers(boolean outFinished, boolean inFinished, int streamId,
- int associatedStreamId, int priority, List<Header> headerBlock,
- HeadersMode headersMode) {
- assertFalse(outFinished);
- assertFalse(inFinished);
- assertEquals(expectedStreamId, streamId);
- assertEquals(-1, associatedStreamId);
- assertEquals(-1, priority);
- assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock);
- assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
- }
- });
- }
-
- @Test public void pushPromise() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final int expectedPromisedStreamId = 11;
-
- final List<Header> pushPromise = Arrays.asList(
- new Header(Header.TARGET_METHOD, "GET"),
- new Header(Header.TARGET_SCHEME, "https"),
- new Header(Header.TARGET_AUTHORITY, "squareup.com"),
- new Header(Header.TARGET_PATH, "/")
- );
-
- { // Write the push promise frame, specifying the associated stream ID.
- OkBuffer headerBytes = literalHeaders(pushPromise);
- frame.writeShort((int) (headerBytes.size() + 4));
- frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE);
- frame.writeByte(Http20Draft09.FLAG_END_PUSH_PROMISE);
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
- frame.write(headerBytes, headerBytes.size());
- }
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Consume the headers frame.
- fr.nextFrame(new BaseTestHandler() {
- @Override
- public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
- assertEquals(expectedStreamId, streamId);
- assertEquals(expectedPromisedStreamId, promisedStreamId);
- assertEquals(pushPromise, headerBlock);
- }
- });
- }
-
- /** Headers are compressed, then framed. */
- @Test public void pushPromiseThenContinuation() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final int expectedPromisedStreamId = 11;
-
- final List<Header> pushPromise = Arrays.asList(
- new Header(Header.TARGET_METHOD, "GET"),
- new Header(Header.TARGET_SCHEME, "https"),
- new Header(Header.TARGET_AUTHORITY, "squareup.com"),
- new Header(Header.TARGET_PATH, "/")
- );
-
- // Decoding the first header will cross frame boundaries.
- OkBuffer headerBlock = literalHeaders(pushPromise);
- int firstFrameLength = (int) (headerBlock.size() - 1);
- { // Write the first headers frame.
- frame.writeShort(firstFrameLength + 4);
- frame.writeByte(Http20Draft09.TYPE_PUSH_PROMISE);
- frame.writeByte(0); // no flags
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
- frame.write(headerBlock, firstFrameLength);
- }
-
- { // Write the continuation frame, specifying no more frames are expected.
- frame.writeShort(1);
- frame.writeByte(Http20Draft09.TYPE_CONTINUATION);
- frame.writeByte(Http20Draft09.FLAG_END_HEADERS);
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.write(headerBlock, 1);
- }
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Reading the above frames should result in a concatenated headerBlock.
- fr.nextFrame(new BaseTestHandler() {
- @Override
- public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
- assertEquals(expectedStreamId, streamId);
- assertEquals(expectedPromisedStreamId, promisedStreamId);
- assertEquals(pushPromise, headerBlock);
- }
- });
- }
-
- @Test public void readRstStreamFrame() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- frame.writeShort(4);
- frame.writeByte(Http20Draft09.TYPE_RST_STREAM);
- frame.writeByte(0); // No flags
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode);
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Consume the reset frame.
- fr.nextFrame(new BaseTestHandler() {
- @Override public void rstStream(int streamId, ErrorCode errorCode) {
- assertEquals(expectedStreamId, streamId);
- assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode);
- }
- });
- }
-
- @Test public void readSettingsFrame() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final int reducedTableSizeBytes = 16;
-
- frame.writeShort(16); // 2 settings * 4 bytes for the code and 4 for the value.
- frame.writeByte(Http20Draft09.TYPE_SETTINGS);
- frame.writeByte(0); // No flags
- frame.writeInt(0 & 0x7fffffff); // Settings are always on the connection stream 0.
- frame.writeInt(Settings.HEADER_TABLE_SIZE & 0xffffff);
- frame.writeInt(reducedTableSizeBytes);
- frame.writeInt(Settings.ENABLE_PUSH & 0xffffff);
- frame.writeInt(0);
-
- final Http20Draft09.Reader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- // Consume the settings frame.
- fr.nextFrame(new BaseTestHandler() {
- @Override public void settings(boolean clearPrevious, Settings settings) {
- assertFalse(clearPrevious); // No clearPrevious in http/2.
- assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize());
- assertEquals(false, settings.getEnablePush(true));
- }
- });
- }
-
- @Test public void pingRoundTrip() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final int expectedPayload1 = 7;
- final int expectedPayload2 = 8;
-
- // Compose the expected PING frame.
- frame.writeShort(8); // length
- frame.writeByte(Http20Draft09.TYPE_PING);
- frame.writeByte(Http20Draft09.FLAG_ACK);
- frame.writeInt(0); // connection-level
- frame.writeInt(expectedPayload1);
- frame.writeInt(expectedPayload2);
-
- // Check writer sends the same bytes.
- assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2));
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- fr.nextFrame(new BaseTestHandler() { // Consume the ping frame.
- @Override public void ping(boolean ack, int payload1, int payload2) {
- assertTrue(ack);
- assertEquals(expectedPayload1, payload1);
- assertEquals(expectedPayload2, payload2);
- }
- });
- }
-
- @Test public void maxLengthDataFrame() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final byte[] expectedData = new byte[16383];
- Arrays.fill(expectedData, (byte) 2);
-
- // Write the data frame.
- frame.writeShort(expectedData.length);
- frame.writeByte(Http20Draft09.TYPE_DATA);
- frame.writeByte(0); // no flags
- frame.writeInt(expectedStreamId & 0x7fffffff);
- frame.write(expectedData);
-
- // Check writer sends the same bytes.
- assertEquals(frame, sendDataFrame(new OkBuffer().write(expectedData)));
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- fr.nextFrame(new BaseTestHandler() {
- @Override public void data(
- boolean inFinished, int streamId, BufferedSource source, int length) throws IOException {
- assertFalse(inFinished);
- assertEquals(expectedStreamId, streamId);
- assertEquals(16383, length);
- ByteString data = source.readByteString(length);
- for (byte b : data.toByteArray()){
- assertEquals(2, b);
- }
- }
- });
- }
-
- @Test public void tooLargeDataFrame() throws IOException {
- try {
- sendDataFrame(new OkBuffer().write(new byte[0x1000000]));
- fail();
- } catch (IllegalArgumentException e) {
- assertEquals("FRAME_SIZE_ERROR length > 16383: 16777216", e.getMessage());
- }
- }
-
- @Test public void windowUpdateRoundTrip() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final long expectedWindowSizeIncrement = 0x7fffffff;
-
- // Compose the expected window update frame.
- frame.writeShort(4); // length
- frame.writeByte(Http20Draft09.TYPE_WINDOW_UPDATE);
- frame.writeByte(0); // No flags.
- frame.writeInt(expectedStreamId);
- frame.writeInt((int) expectedWindowSizeIncrement);
-
- // Check writer sends the same bytes.
- assertEquals(frame, windowUpdate(expectedWindowSizeIncrement));
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- fr.nextFrame(new BaseTestHandler() { // Consume the window update frame.
- @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
- assertEquals(expectedStreamId, streamId);
- assertEquals(expectedWindowSizeIncrement, windowSizeIncrement);
- }
- });
- }
-
- @Test public void badWindowSizeIncrement() throws IOException {
- try {
- windowUpdate(0);
- fail();
- } catch (IllegalArgumentException e) {
- assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0",
- e.getMessage());
- }
- try {
- windowUpdate(0x80000000L);
- fail();
- } catch (IllegalArgumentException e) {
- assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648",
- e.getMessage());
- }
- }
-
- @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
-
- // Compose the expected GOAWAY frame without debug data.
- frame.writeShort(8); // Without debug data there's only 2 32-bit fields.
- frame.writeByte(Http20Draft09.TYPE_GOAWAY);
- frame.writeByte(0); // no flags.
- frame.writeInt(0); // connection-scope
- frame.writeInt(expectedStreamId); // last good stream.
- frame.writeInt(expectedError.httpCode);
-
- // Check writer sends the same bytes.
- assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY));
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- fr.nextFrame(new BaseTestHandler() { // Consume the go away frame.
- @Override public void goAway(
- int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
- assertEquals(expectedStreamId, lastGoodStreamId);
- assertEquals(expectedError, errorCode);
- assertEquals(0, debugData.size());
- }
- });
- }
-
- @Test public void goAwayWithDebugDataRoundTrip() throws IOException {
- OkBuffer frame = new OkBuffer();
-
- final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
- final ByteString expectedData = ByteString.encodeUtf8("abcdefgh");
-
- // Compose the expected GOAWAY frame without debug data.
- frame.writeShort(8 + expectedData.size());
- frame.writeByte(Http20Draft09.TYPE_GOAWAY);
- frame.writeByte(0); // no flags.
- frame.writeInt(0); // connection-scope
- frame.writeInt(0); // never read any stream!
- frame.writeInt(expectedError.httpCode);
- frame.write(expectedData.toByteArray());
-
- // Check writer sends the same bytes.
- assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray()));
-
- FrameReader fr = new Http20Draft09.Reader(frame, 4096, false);
-
- fr.nextFrame(new BaseTestHandler() { // Consume the go away frame.
- @Override public void goAway(
- int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
- assertEquals(0, lastGoodStreamId);
- assertEquals(expectedError, errorCode);
- assertEquals(expectedData, debugData);
- }
- });
- }
-
- @Test public void frameSizeError() throws IOException {
- Http20Draft09.Writer writer = new Http20Draft09.Writer(new OkBuffer(), true);
-
- try {
- writer.frameHeader(16384, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, 0);
- fail();
- } catch (IllegalArgumentException e) {
- assertEquals("FRAME_SIZE_ERROR length > 16383: 16384", e.getMessage());
- }
- }
-
- @Test public void streamIdHasReservedBit() throws IOException {
- Http20Draft09.Writer writer = new Http20Draft09.Writer(new OkBuffer(), true);
-
- try {
- int streamId = 3;
- streamId |= 1L << 31; // set reserved bit
- writer.frameHeader(16383, Http20Draft09.TYPE_DATA, Http20Draft09.FLAG_NONE, streamId);
- fail();
- } catch (IllegalArgumentException e) {
- assertEquals("reserved bit set: -2147483645", e.getMessage());
- }
- }
-
- private OkBuffer literalHeaders(List<Header> sentHeaders) throws IOException {
- OkBuffer out = new OkBuffer();
- new HpackDraft05.Writer(out).writeHeaders(sentHeaders);
- return out;
- }
-
- private OkBuffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException {
- OkBuffer out = new OkBuffer();
- new Http20Draft09.Writer(out, true).ping(ack, payload1, payload2);
- return out;
- }
-
- private OkBuffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
- throws IOException {
- OkBuffer out = new OkBuffer();
- new Http20Draft09.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData);
- return out;
- }
-
- private OkBuffer sendDataFrame(OkBuffer data) throws IOException {
- OkBuffer out = new OkBuffer();
- new Http20Draft09.Writer(out, true).dataFrame(expectedStreamId, Http20Draft09.FLAG_NONE, data,
- (int) data.size());
- return out;
- }
-
- private OkBuffer windowUpdate(long windowSizeIncrement) throws IOException {
- OkBuffer out = new OkBuffer();
- new Http20Draft09.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement);
- return out;
- }
-}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft16FrameLoggerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft16FrameLoggerTest.java
new file mode 100644
index 0000000..992611f
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft16FrameLoggerTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_ACK;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_END_HEADERS;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_END_STREAM;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_NONE;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FrameLogger.formatFlags;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FrameLogger.formatHeader;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_CONTINUATION;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_DATA;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_GOAWAY;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_HEADERS;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_PING;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_PUSH_PROMISE;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.TYPE_SETTINGS;
+import static org.junit.Assert.assertEquals;
+
+public class Http20Draft16FrameLoggerTest {
+
+ /** Real stream traffic applied to the log format. */
+ @Test public void exampleStream() {
+ assertEquals(">> 0x00000000 5 SETTINGS ",
+ formatHeader(false, 0, 5, TYPE_SETTINGS, FLAG_NONE));
+ assertEquals(">> 0x00000003 100 HEADERS END_HEADERS",
+ formatHeader(false, 3, 100, TYPE_HEADERS, FLAG_END_HEADERS));
+ assertEquals(">> 0x00000003 0 DATA END_STREAM",
+ formatHeader(false, 3, 0, TYPE_DATA, FLAG_END_STREAM));
+ assertEquals("<< 0x00000000 15 SETTINGS ",
+ formatHeader(true, 0, 15, TYPE_SETTINGS, FLAG_NONE));
+ assertEquals(">> 0x00000000 0 SETTINGS ACK",
+ formatHeader(false, 0, 0, TYPE_SETTINGS, FLAG_ACK));
+ assertEquals("<< 0x00000000 0 SETTINGS ACK",
+ formatHeader(true, 0, 0, TYPE_SETTINGS, FLAG_ACK));
+ assertEquals("<< 0x00000003 22 HEADERS END_HEADERS",
+ formatHeader(true, 3, 22, TYPE_HEADERS, FLAG_END_HEADERS));
+ assertEquals("<< 0x00000003 226 DATA END_STREAM",
+ formatHeader(true, 3, 226, TYPE_DATA, FLAG_END_STREAM));
+ assertEquals(">> 0x00000000 8 GOAWAY ",
+ formatHeader(false, 0, 8, TYPE_GOAWAY, FLAG_NONE));
+ }
+
+ @Test public void flagOverlapOn0x1() {
+ assertEquals("<< 0x00000000 0 SETTINGS ACK",
+ formatHeader(true, 0, 0, TYPE_SETTINGS, (byte) 0x1));
+ assertEquals("<< 0x00000000 8 PING ACK",
+ formatHeader(true, 0, 8, TYPE_PING, (byte) 0x1));
+ assertEquals("<< 0x00000003 0 HEADERS END_STREAM",
+ formatHeader(true, 3, 0, TYPE_HEADERS, (byte) 0x1));
+ assertEquals("<< 0x00000003 0 DATA END_STREAM",
+ formatHeader(true, 3, 0, TYPE_DATA, (byte) 0x1));
+ }
+
+ @Test public void flagOverlapOn0x4() {
+ assertEquals("<< 0x00000003 10000 HEADERS END_HEADERS",
+ formatHeader(true, 3, 10000, TYPE_HEADERS, (byte) 0x4));
+ assertEquals("<< 0x00000003 10000 CONTINUATION END_HEADERS",
+ formatHeader(true, 3, 10000, TYPE_CONTINUATION, (byte) 0x4));
+ assertEquals("<< 0x00000004 10000 PUSH_PROMISE END_PUSH_PROMISE",
+ formatHeader(true, 4, 10000, TYPE_PUSH_PROMISE, (byte) 0x4));
+ }
+
+ @Test public void flagOverlapOn0x20() {
+ assertEquals("<< 0x00000003 10000 HEADERS PRIORITY",
+ formatHeader(true, 3, 10000, TYPE_HEADERS, (byte) 0x20));
+ assertEquals("<< 0x00000003 10000 DATA COMPRESSED",
+ formatHeader(true, 3, 10000, TYPE_DATA, (byte) 0x20));
+ }
+
+ /**
+ * Ensures that valid flag combinations appear visually correct, and invalid show in hex. This
+ * also demonstrates how sparse the lookup table is.
+ */
+ @Test public void allFormattedFlagsWithValidBits() {
+ List<String> formattedFlags = new ArrayList<>(0x40); // Highest valid flag is 0x20.
+ for (byte i = 0; i < 0x40; i++) formattedFlags.add(formatFlags(TYPE_HEADERS, i));
+
+ assertEquals(Arrays.asList(
+ "",
+ "END_STREAM",
+ "00000010",
+ "00000011",
+ "END_HEADERS",
+ "END_STREAM|END_HEADERS",
+ "00000110",
+ "00000111",
+ "PADDED",
+ "END_STREAM|PADDED",
+ "00001010",
+ "00001011",
+ "00001100",
+ "END_STREAM|END_HEADERS|PADDED",
+ "00001110",
+ "00001111",
+ "00010000",
+ "00010001",
+ "00010010",
+ "00010011",
+ "00010100",
+ "00010101",
+ "00010110",
+ "00010111",
+ "00011000",
+ "00011001",
+ "00011010",
+ "00011011",
+ "00011100",
+ "00011101",
+ "00011110",
+ "00011111",
+ "PRIORITY",
+ "END_STREAM|PRIORITY",
+ "00100010",
+ "00100011",
+ "END_HEADERS|PRIORITY",
+ "END_STREAM|END_HEADERS|PRIORITY",
+ "00100110",
+ "00100111",
+ "00101000",
+ "END_STREAM|PRIORITY|PADDED",
+ "00101010",
+ "00101011",
+ "00101100",
+ "END_STREAM|END_HEADERS|PRIORITY|PADDED",
+ "00101110",
+ "00101111",
+ "00110000",
+ "00110001",
+ "00110010",
+ "00110011",
+ "00110100",
+ "00110101",
+ "00110110",
+ "00110111",
+ "00111000",
+ "00111001",
+ "00111010",
+ "00111011",
+ "00111100",
+ "00111101",
+ "00111110",
+ "00111111"
+ ), formattedFlags);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft16Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft16Test.java
new file mode 100644
index 0000000..acf3f1c
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http20Draft16Test.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.GzipSink;
+import okio.Okio;
+import org.junit.Test;
+
+import static com.squareup.okhttp.TestUtil.headerEntries;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_COMPRESSED;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_END_HEADERS;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_END_STREAM;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_NONE;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_PADDED;
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FLAG_PRIORITY;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class Http20Draft16Test {
+ final Buffer frame = new Buffer();
+ final FrameReader fr = new Http20Draft16.Reader(frame, 4096, false);
+ final int expectedStreamId = 15;
+
+ @Test public void unknownFrameTypeSkipped() throws IOException {
+ writeMedium(frame, 4); // has a 4-byte field
+ frame.writeByte(99); // type 99
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(expectedStreamId);
+ frame.writeInt(111111111); // custom data
+
+ fr.nextFrame(new BaseTestHandler()); // Should not callback.
+ }
+
+ @Test public void onlyOneLiteralHeadersFrame() throws IOException {
+ final List<Header> sentHeaders = headerEntries("name", "value");
+
+ Buffer headerBytes = literalHeaders(sentHeaders);
+ writeMedium(frame, (int) headerBytes.size());
+ frame.writeByte(Http20Draft16.TYPE_HEADERS);
+ frame.writeByte(FLAG_END_HEADERS | FLAG_END_STREAM);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeAll(headerBytes);
+
+ assertEquals(frame, sendHeaderFrames(true, sentHeaders)); // Check writer sends the same bytes.
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override
+ public void headers(boolean outFinished, boolean inFinished, int streamId,
+ int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
+ assertFalse(outFinished);
+ assertTrue(inFinished);
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(-1, associatedStreamId);
+ assertEquals(sentHeaders, headerBlock);
+ assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+ }
+ });
+ }
+
+ @Test public void headersWithPriority() throws IOException {
+ final List<Header> sentHeaders = headerEntries("name", "value");
+
+ Buffer headerBytes = literalHeaders(sentHeaders);
+ writeMedium(frame, (int) (headerBytes.size() + 5));
+ frame.writeByte(Http20Draft16.TYPE_HEADERS);
+ frame.writeByte(FLAG_END_HEADERS | FLAG_PRIORITY);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeInt(0); // Independent stream.
+ frame.writeByte(255); // Heaviest weight, zero-indexed.
+ frame.writeAll(headerBytes);
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void priority(int streamId, int streamDependency, int weight,
+ boolean exclusive) {
+ assertEquals(0, streamDependency);
+ assertEquals(256, weight);
+ assertFalse(exclusive);
+ }
+
+ @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
+ int associatedStreamId, List<Header> nameValueBlock,
+ HeadersMode headersMode) {
+ assertFalse(outFinished);
+ assertFalse(inFinished);
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(-1, associatedStreamId);
+ assertEquals(sentHeaders, nameValueBlock);
+ assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+ }
+ });
+ }
+
+ /** Headers are compressed, then framed. */
+ @Test public void headersFrameThenContinuation() throws IOException {
+ final List<Header> sentHeaders = largeHeaders();
+
+ Buffer headerBlock = literalHeaders(sentHeaders);
+
+ // Write the first headers frame.
+ writeMedium(frame, Http20Draft16.INITIAL_MAX_FRAME_SIZE);
+ frame.writeByte(Http20Draft16.TYPE_HEADERS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.write(headerBlock, Http20Draft16.INITIAL_MAX_FRAME_SIZE);
+
+ // Write the continuation frame, specifying no more frames are expected.
+ writeMedium(frame, (int) headerBlock.size());
+ frame.writeByte(Http20Draft16.TYPE_CONTINUATION);
+ frame.writeByte(FLAG_END_HEADERS);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeAll(headerBlock);
+
+ assertEquals(frame, sendHeaderFrames(false, sentHeaders)); // Check writer sends the same bytes.
+
+ // Reading the above frames should result in a concatenated headerBlock.
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
+ int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
+ assertFalse(outFinished);
+ assertFalse(inFinished);
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(-1, associatedStreamId);
+ assertEquals(sentHeaders, headerBlock);
+ assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+ }
+ });
+ }
+
+ @Test public void pushPromise() throws IOException {
+ final int expectedPromisedStreamId = 11;
+
+ final List<Header> pushPromise = Arrays.asList(
+ new Header(Header.TARGET_METHOD, "GET"),
+ new Header(Header.TARGET_SCHEME, "https"),
+ new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+ new Header(Header.TARGET_PATH, "/")
+ );
+
+ // Write the push promise frame, specifying the associated stream ID.
+ Buffer headerBytes = literalHeaders(pushPromise);
+ writeMedium(frame, (int) (headerBytes.size() + 4));
+ frame.writeByte(Http20Draft16.TYPE_PUSH_PROMISE);
+ frame.writeByte(Http20Draft16.FLAG_END_PUSH_PROMISE);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
+ frame.writeAll(headerBytes);
+
+ assertEquals(frame, sendPushPromiseFrames(expectedPromisedStreamId, pushPromise));
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override
+ public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(expectedPromisedStreamId, promisedStreamId);
+ assertEquals(pushPromise, headerBlock);
+ }
+ });
+ }
+
+ /** Headers are compressed, then framed. */
+ @Test public void pushPromiseThenContinuation() throws IOException {
+ final int expectedPromisedStreamId = 11;
+ final List<Header> pushPromise = largeHeaders();
+
+ // Decoding the first header will cross frame boundaries.
+ Buffer headerBlock = literalHeaders(pushPromise);
+
+ // Write the first headers frame.
+ writeMedium(frame, Http20Draft16.INITIAL_MAX_FRAME_SIZE);
+ frame.writeByte(Http20Draft16.TYPE_PUSH_PROMISE);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeInt(expectedPromisedStreamId & 0x7fffffff);
+ frame.write(headerBlock, Http20Draft16.INITIAL_MAX_FRAME_SIZE - 4);
+
+ // Write the continuation frame, specifying no more frames are expected.
+ writeMedium(frame, (int) headerBlock.size());
+ frame.writeByte(Http20Draft16.TYPE_CONTINUATION);
+ frame.writeByte(FLAG_END_HEADERS);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeAll(headerBlock);
+
+ assertEquals(frame, sendPushPromiseFrames(expectedPromisedStreamId, pushPromise));
+
+ // Reading the above frames should result in a concatenated headerBlock.
+ fr.nextFrame(new BaseTestHandler() {
+ @Override
+ public void pushPromise(int streamId, int promisedStreamId, List<Header> headerBlock) {
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(expectedPromisedStreamId, promisedStreamId);
+ assertEquals(pushPromise, headerBlock);
+ }
+ });
+ }
+
+ @Test public void readRstStreamFrame() throws IOException {
+ writeMedium(frame, 4);
+ frame.writeByte(Http20Draft16.TYPE_RST_STREAM);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeInt(ErrorCode.COMPRESSION_ERROR.httpCode);
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void rstStream(int streamId, ErrorCode errorCode) {
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(ErrorCode.COMPRESSION_ERROR, errorCode);
+ }
+ });
+ }
+
+ @Test public void readSettingsFrame() throws IOException {
+ final int reducedTableSizeBytes = 16;
+
+ writeMedium(frame, 12); // 2 settings * 6 bytes (2 for the code and 4 for the value).
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(1); // SETTINGS_HEADER_TABLE_SIZE
+ frame.writeInt(reducedTableSizeBytes);
+ frame.writeShort(2); // SETTINGS_ENABLE_PUSH
+ frame.writeInt(0);
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void settings(boolean clearPrevious, Settings settings) {
+ assertFalse(clearPrevious); // No clearPrevious in HTTP/2.
+ assertEquals(reducedTableSizeBytes, settings.getHeaderTableSize());
+ assertEquals(false, settings.getEnablePush(true));
+ }
+ });
+ }
+
+ @Test public void readSettingsFrameInvalidPushValue() throws IOException {
+ writeMedium(frame, 6); // 2 for the code and 4 for the value
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(2);
+ frame.writeInt(2);
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1", e.getMessage());
+ }
+ }
+
+ @Test public void readSettingsFrameInvalidSettingId() throws IOException {
+ writeMedium(frame, 6); // 2 for the code and 4 for the value
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(7); // old number for SETTINGS_INITIAL_WINDOW_SIZE
+ frame.writeInt(1);
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR invalid settings id: 7", e.getMessage());
+ }
+ }
+
+ @Test public void readSettingsFrameNegativeWindowSize() throws IOException {
+ writeMedium(frame, 6); // 2 for the code and 4 for the value
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(4); // SETTINGS_INITIAL_WINDOW_SIZE
+ frame.writeInt(Integer.MIN_VALUE);
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1", e.getMessage());
+ }
+ }
+
+ @Test public void readSettingsFrameNegativeFrameLength() throws IOException {
+ writeMedium(frame, 6); // 2 for the code and 4 for the value
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(5); // SETTINGS_MAX_FRAME_SIZE
+ frame.writeInt(Integer.MIN_VALUE);
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: -2147483648", e.getMessage());
+ }
+ }
+
+ @Test public void readSettingsFrameTooShortFrameLength() throws IOException {
+ writeMedium(frame, 6); // 2 for the code and 4 for the value
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(5); // SETTINGS_MAX_FRAME_SIZE
+ frame.writeInt((int) Math.pow(2, 14) - 1);
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: 16383", e.getMessage());
+ }
+ }
+
+ @Test public void readSettingsFrameTooLongFrameLength() throws IOException {
+ writeMedium(frame, 6); // 2 for the code and 4 for the value
+ frame.writeByte(Http20Draft16.TYPE_SETTINGS);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // Settings are always on the connection stream 0.
+ frame.writeShort(5); // SETTINGS_MAX_FRAME_SIZE
+ frame.writeInt((int) Math.pow(2, 24));
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: 16777216", e.getMessage());
+ }
+ }
+
+ @Test public void pingRoundTrip() throws IOException {
+ final int expectedPayload1 = 7;
+ final int expectedPayload2 = 8;
+
+ writeMedium(frame, 8); // length
+ frame.writeByte(Http20Draft16.TYPE_PING);
+ frame.writeByte(Http20Draft16.FLAG_ACK);
+ frame.writeInt(0); // connection-level
+ frame.writeInt(expectedPayload1);
+ frame.writeInt(expectedPayload2);
+
+ // Check writer sends the same bytes.
+ assertEquals(frame, sendPingFrame(true, expectedPayload1, expectedPayload2));
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void ping(boolean ack, int payload1, int payload2) {
+ assertTrue(ack);
+ assertEquals(expectedPayload1, payload1);
+ assertEquals(expectedPayload2, payload2);
+ }
+ });
+ }
+
+ @Test public void maxLengthDataFrame() throws IOException {
+ final byte[] expectedData = new byte[Http20Draft16.INITIAL_MAX_FRAME_SIZE];
+ Arrays.fill(expectedData, (byte) 2);
+
+ writeMedium(frame, expectedData.length);
+ frame.writeByte(Http20Draft16.TYPE_DATA);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.write(expectedData);
+
+ // Check writer sends the same bytes.
+ assertEquals(frame, sendDataFrame(new Buffer().write(expectedData)));
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void data(boolean inFinished, int streamId, BufferedSource source,
+ int length) throws IOException {
+ assertFalse(inFinished);
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(Http20Draft16.INITIAL_MAX_FRAME_SIZE, length);
+ ByteString data = source.readByteString(length);
+ for (byte b : data.toByteArray()) {
+ assertEquals(2, b);
+ }
+ }
+ });
+ }
+
+ /** We do not send SETTINGS_COMPRESS_DATA = 1, nor want to. Let's make sure we error. */
+ @Test public void compressedDataFrameWhenSettingDisabled() throws IOException {
+ byte[] expectedData = new byte[Http20Draft16.INITIAL_MAX_FRAME_SIZE];
+ Arrays.fill(expectedData, (byte) 2);
+ Buffer zipped = gzip(expectedData);
+ int zippedSize = (int) zipped.size();
+
+ writeMedium(frame, zippedSize);
+ frame.writeByte(Http20Draft16.TYPE_DATA);
+ frame.writeByte(FLAG_COMPRESSED);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ zipped.readAll(frame);
+
+ try {
+ fr.nextFrame(new BaseTestHandler());
+ fail();
+ } catch (IOException e) {
+ assertEquals("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA",
+ e.getMessage());
+ }
+ }
+
+ @Test public void readPaddedDataFrame() throws IOException {
+ int dataLength = 1123;
+ byte[] expectedData = new byte[dataLength];
+ Arrays.fill(expectedData, (byte) 2);
+
+ int paddingLength = 254;
+ byte[] padding = new byte[paddingLength];
+ Arrays.fill(padding, (byte) 0);
+
+ writeMedium(frame, dataLength + paddingLength + 1);
+ frame.writeByte(Http20Draft16.TYPE_DATA);
+ frame.writeByte(FLAG_PADDED);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeByte(paddingLength);
+ frame.write(expectedData);
+ frame.write(padding);
+
+ fr.nextFrame(assertData());
+ assertTrue(frame.exhausted()); // Padding was skipped.
+ }
+
+ @Test public void readPaddedDataFrameZeroPadding() throws IOException {
+ int dataLength = 1123;
+ byte[] expectedData = new byte[dataLength];
+ Arrays.fill(expectedData, (byte) 2);
+
+ writeMedium(frame, dataLength + 1);
+ frame.writeByte(Http20Draft16.TYPE_DATA);
+ frame.writeByte(FLAG_PADDED);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeByte(0);
+ frame.write(expectedData);
+
+ fr.nextFrame(assertData());
+ }
+
+ @Test public void readPaddedHeadersFrame() throws IOException {
+ int paddingLength = 254;
+ byte[] padding = new byte[paddingLength];
+ Arrays.fill(padding, (byte) 0);
+
+ Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
+ writeMedium(frame, (int) headerBlock.size() + paddingLength + 1);
+ frame.writeByte(Http20Draft16.TYPE_HEADERS);
+ frame.writeByte(FLAG_END_HEADERS | FLAG_PADDED);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeByte(paddingLength);
+ frame.writeAll(headerBlock);
+ frame.write(padding);
+
+ fr.nextFrame(assertHeaderBlock());
+ assertTrue(frame.exhausted()); // Padding was skipped.
+ }
+
+ @Test public void readPaddedHeadersFrameZeroPadding() throws IOException {
+ Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
+ writeMedium(frame, (int) headerBlock.size() + 1);
+ frame.writeByte(Http20Draft16.TYPE_HEADERS);
+ frame.writeByte(FLAG_END_HEADERS | FLAG_PADDED);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeByte(0);
+ frame.writeAll(headerBlock);
+
+ fr.nextFrame(assertHeaderBlock());
+ }
+
+ /** Headers are compressed, then framed. */
+ @Test public void readPaddedHeadersFrameThenContinuation() throws IOException {
+ int paddingLength = 254;
+ byte[] padding = new byte[paddingLength];
+ Arrays.fill(padding, (byte) 0);
+
+ // Decoding the first header will cross frame boundaries.
+ Buffer headerBlock = literalHeaders(headerEntries("foo", "barrr", "baz", "qux"));
+
+ // Write the first headers frame.
+ writeMedium(frame, (int) (headerBlock.size() / 2) + paddingLength + 1);
+ frame.writeByte(Http20Draft16.TYPE_HEADERS);
+ frame.writeByte(FLAG_PADDED);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeByte(paddingLength);
+ frame.write(headerBlock, headerBlock.size() / 2);
+ frame.write(padding);
+
+ // Write the continuation frame, specifying no more frames are expected.
+ writeMedium(frame, (int) headerBlock.size());
+ frame.writeByte(Http20Draft16.TYPE_CONTINUATION);
+ frame.writeByte(FLAG_END_HEADERS);
+ frame.writeInt(expectedStreamId & 0x7fffffff);
+ frame.writeAll(headerBlock);
+
+ fr.nextFrame(assertHeaderBlock());
+ assertTrue(frame.exhausted());
+ }
+
+ @Test public void tooLargeDataFrame() throws IOException {
+ try {
+ sendDataFrame(new Buffer().write(new byte[0x1000000]));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("FRAME_SIZE_ERROR length > 16384: 16777216", e.getMessage());
+ }
+ }
+
+ @Test public void windowUpdateRoundTrip() throws IOException {
+ final long expectedWindowSizeIncrement = 0x7fffffff;
+
+ writeMedium(frame, 4); // length
+ frame.writeByte(Http20Draft16.TYPE_WINDOW_UPDATE);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(expectedStreamId);
+ frame.writeInt((int) expectedWindowSizeIncrement);
+
+ // Check writer sends the same bytes.
+ assertEquals(frame, windowUpdate(expectedWindowSizeIncrement));
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void windowUpdate(int streamId, long windowSizeIncrement) {
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(expectedWindowSizeIncrement, windowSizeIncrement);
+ }
+ });
+ }
+
+ @Test public void badWindowSizeIncrement() throws IOException {
+ try {
+ windowUpdate(0);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 0",
+ e.getMessage());
+ }
+ try {
+ windowUpdate(0x80000000L);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: 2147483648",
+ e.getMessage());
+ }
+ }
+
+ @Test public void goAwayWithoutDebugDataRoundTrip() throws IOException {
+ final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
+
+ writeMedium(frame, 8); // Without debug data there's only 2 32-bit fields.
+ frame.writeByte(Http20Draft16.TYPE_GOAWAY);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // connection-scope
+ frame.writeInt(expectedStreamId); // last good stream.
+ frame.writeInt(expectedError.httpCode);
+
+ // Check writer sends the same bytes.
+ assertEquals(frame, sendGoAway(expectedStreamId, expectedError, Util.EMPTY_BYTE_ARRAY));
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void goAway(
+ int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+ assertEquals(expectedStreamId, lastGoodStreamId);
+ assertEquals(expectedError, errorCode);
+ assertEquals(0, debugData.size());
+ }
+ });
+ }
+
+ @Test public void goAwayWithDebugDataRoundTrip() throws IOException {
+ final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
+ final ByteString expectedData = ByteString.encodeUtf8("abcdefgh");
+
+ // Compose the expected GOAWAY frame without debug data.
+ writeMedium(frame, 8 + expectedData.size());
+ frame.writeByte(Http20Draft16.TYPE_GOAWAY);
+ frame.writeByte(Http20Draft16.FLAG_NONE);
+ frame.writeInt(0); // connection-scope
+ frame.writeInt(0); // never read any stream!
+ frame.writeInt(expectedError.httpCode);
+ frame.write(expectedData.toByteArray());
+
+ // Check writer sends the same bytes.
+ assertEquals(frame, sendGoAway(0, expectedError, expectedData.toByteArray()));
+
+ fr.nextFrame(new BaseTestHandler() {
+ @Override public void goAway(
+ int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
+ assertEquals(0, lastGoodStreamId);
+ assertEquals(expectedError, errorCode);
+ assertEquals(expectedData, debugData);
+ }
+ });
+ }
+
+ @Test public void frameSizeError() throws IOException {
+ Http20Draft16.Writer writer = new Http20Draft16.Writer(new Buffer(), true);
+
+ try {
+ writer.frameHeader(0, 16777216, Http20Draft16.TYPE_DATA, FLAG_NONE);
+ fail();
+ } catch (IllegalArgumentException e) {
+ // TODO: real max is based on settings between 16384 and 16777215
+ assertEquals("FRAME_SIZE_ERROR length > 16384: 16777216", e.getMessage());
+ }
+ }
+
+ @Test public void ackSettingsAppliesMaxFrameSize() throws IOException {
+ int newMaxFrameSize = 16777215;
+
+ Http20Draft16.Writer writer = new Http20Draft16.Writer(new Buffer(), true);
+
+ writer.ackSettings(new Settings().set(Settings.MAX_FRAME_SIZE, 0, newMaxFrameSize));
+
+ assertEquals(newMaxFrameSize, writer.maxDataLength());
+ writer.frameHeader(0, newMaxFrameSize, Http20Draft16.TYPE_DATA, FLAG_NONE);
+ }
+
+ @Test public void streamIdHasReservedBit() throws IOException {
+ Http20Draft16.Writer writer = new Http20Draft16.Writer(new Buffer(), true);
+
+ try {
+ int streamId = 3;
+ streamId |= 1L << 31; // set reserved bit
+ writer.frameHeader(streamId, Http20Draft16.INITIAL_MAX_FRAME_SIZE, Http20Draft16.TYPE_DATA, FLAG_NONE);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("reserved bit set: -2147483645", e.getMessage());
+ }
+ }
+
+ private Buffer literalHeaders(List<Header> sentHeaders) throws IOException {
+ Buffer out = new Buffer();
+ new HpackDraft10.Writer(out).writeHeaders(sentHeaders);
+ return out;
+ }
+
+ private Buffer sendHeaderFrames(boolean outFinished, List<Header> headers) throws IOException {
+ Buffer out = new Buffer();
+ new Http20Draft16.Writer(out, true).headers(outFinished, expectedStreamId, headers);
+ return out;
+ }
+
+ private Buffer sendPushPromiseFrames(int streamId, List<Header> headers) throws IOException {
+ Buffer out = new Buffer();
+ new Http20Draft16.Writer(out, true).pushPromise(expectedStreamId, streamId, headers);
+ return out;
+ }
+
+ private Buffer sendPingFrame(boolean ack, int payload1, int payload2) throws IOException {
+ Buffer out = new Buffer();
+ new Http20Draft16.Writer(out, true).ping(ack, payload1, payload2);
+ return out;
+ }
+
+ private Buffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
+ throws IOException {
+ Buffer out = new Buffer();
+ new Http20Draft16.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData);
+ return out;
+ }
+
+ private Buffer sendDataFrame(Buffer data) throws IOException {
+ Buffer out = new Buffer();
+ new Http20Draft16.Writer(out, true).dataFrame(expectedStreamId, FLAG_NONE, data,
+ (int) data.size());
+ return out;
+ }
+
+ private Buffer windowUpdate(long windowSizeIncrement) throws IOException {
+ Buffer out = new Buffer();
+ new Http20Draft16.Writer(out, true).windowUpdate(expectedStreamId, windowSizeIncrement);
+ return out;
+ }
+
+ private FrameReader.Handler assertHeaderBlock() {
+ return new BaseTestHandler() {
+ @Override public void headers(boolean outFinished, boolean inFinished, int streamId,
+ int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
+ assertFalse(outFinished);
+ assertFalse(inFinished);
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(-1, associatedStreamId);
+ assertEquals(headerEntries("foo", "barrr", "baz", "qux"), headerBlock);
+ assertEquals(HeadersMode.HTTP_20_HEADERS, headersMode);
+ }
+ };
+ }
+
+ private FrameReader.Handler assertData() {
+ return new BaseTestHandler() {
+ @Override public void data(boolean inFinished, int streamId, BufferedSource source,
+ int length) throws IOException {
+ assertFalse(inFinished);
+ assertEquals(expectedStreamId, streamId);
+ assertEquals(1123, length);
+ ByteString data = source.readByteString(length);
+ for (byte b : data.toByteArray()) {
+ assertEquals(2, b);
+ }
+ }
+ };
+ }
+
+ private static Buffer gzip(byte[] data) throws IOException {
+ Buffer buffer = new Buffer();
+ Okio.buffer(new GzipSink(buffer)).write(data).close();
+ return buffer;
+ }
+
+ /** Create a sufficiently large header set to overflow Http20Draft12.INITIAL_MAX_FRAME_SIZE bytes. */
+ private static List<Header> largeHeaders() {
+ String[] nameValues = new String[32];
+ char[] chars = new char[512];
+ for (int i = 0; i < nameValues.length;) {
+ Arrays.fill(chars, (char) i);
+ nameValues[i++] = nameValues[i++] = String.valueOf(chars);
+ }
+ return headerEntries(nameValues);
+ }
+
+ private static void writeMedium(BufferedSink sink, int i) throws IOException {
+ sink.writeByte((i >>> 16) & 0xff);
+ sink.writeByte((i >>> 8) & 0xff);
+ sink.writeByte( i & 0xff);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2ConnectionTest.java
new file mode 100644
index 0000000..13c91d1
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Http2ConnectionTest.java
@@ -0,0 +1,521 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+import org.junit.After;
+import org.junit.Test;
+
+import static com.squareup.okhttp.TestUtil.headerEntries;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.CANCEL;
+import static com.squareup.okhttp.internal.spdy.ErrorCode.PROTOCOL_ERROR;
+import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
+import static com.squareup.okhttp.internal.spdy.Settings.PERSIST_VALUE;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_DATA;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_HEADERS;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_PING;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_SETTINGS;
+import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class Http2ConnectionTest {
+ private static final Variant HTTP_2 = new Http20Draft16();
+ private final MockSpdyPeer peer = new MockSpdyPeer();
+
+ @After public void tearDown() throws Exception {
+ peer.close();
+ }
+
+ @Test public void serverPingsClientHttp2() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // write the mocking script
+ peer.sendFrame().ping(false, 2, 3);
+ peer.acceptFrame(); // PING
+ peer.play();
+
+ // play it back
+ connection(peer, HTTP_2);
+
+ // verify the peer received what was expected
+ MockSpdyPeer.InFrame ping = peer.takeFrame();
+ assertEquals(TYPE_PING, ping.type);
+ assertEquals(0, ping.streamId);
+ assertEquals(2, ping.payload1);
+ assertEquals(3, ping.payload2);
+ assertTrue(ping.ack);
+ }
+
+ @Test public void clientPingsServerHttp2() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // write the mocking script
+ peer.acceptFrame(); // PING
+ peer.sendFrame().ping(true, 1, 5);
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connection(peer, HTTP_2);
+ Ping ping = connection.ping();
+ assertTrue(ping.roundTripTime() > 0);
+ assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
+
+ // verify the peer received what was expected
+ MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
+ assertEquals(0, pingFrame.streamId);
+ assertEquals(1, pingFrame.payload1);
+ assertEquals(0x4f4b6f6b, pingFrame.payload2); // connection.ping() sets this.
+ assertFalse(pingFrame.ack);
+ }
+
+ @Test public void peerHttp2ServerLowersInitialWindowSize() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ Settings initial = new Settings();
+ initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 1684);
+ Settings shouldntImpactConnection = new Settings();
+ shouldntImpactConnection.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 3368);
+
+ peer.sendFrame().settings(initial);
+ peer.acceptFrame(); // ACK
+ peer.sendFrame().settings(shouldntImpactConnection);
+ peer.acceptFrame(); // ACK 2
+ peer.acceptFrame(); // HEADERS
+ peer.play();
+
+ SpdyConnection connection = connection(peer, HTTP_2);
+
+ // Default is 64KiB - 1.
+ assertEquals(65535, connection.peerSettings.getInitialWindowSize(-1));
+
+ // Verify the peer received the ACK.
+ MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
+ assertEquals(TYPE_SETTINGS, ackFrame.type);
+ assertEquals(0, ackFrame.streamId);
+ assertTrue(ackFrame.ack);
+ ackFrame = peer.takeFrame();
+ assertEquals(TYPE_SETTINGS, ackFrame.type);
+ assertEquals(0, ackFrame.streamId);
+ assertTrue(ackFrame.ack);
+
+ // This stream was created *after* the connection settings were adjusted.
+ SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
+
+ assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
+ assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
+ // New Stream is has the most recent initial window size.
+ assertEquals(3368, stream.bytesLeftInWriteWindow);
+ }
+
+ @Test public void peerHttp2ServerZerosCompressionTable() throws Exception {
+ boolean client = false; // Peer is server, so we are client.
+ Settings settings = new Settings();
+ settings.set(Settings.HEADER_TABLE_SIZE, PERSIST_VALUE, 0);
+
+ SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
+
+ // verify the peer's settings were read and applied.
+ assertEquals(0, connection.peerSettings.getHeaderTableSize());
+ Http20Draft16.Reader frameReader = (Http20Draft16.Reader) connection.readerRunnable.frameReader;
+ assertEquals(0, frameReader.hpackReader.maxDynamicTableByteCount());
+ // TODO: when supported, check the frameWriter's compression table is unaffected.
+ }
+
+ @Test public void peerHttp2ClientDisablesPush() throws Exception {
+ boolean client = false; // Peer is client, so we are server.
+ Settings settings = new Settings();
+ settings.set(Settings.ENABLE_PUSH, 0, 0); // The peer client disables push.
+
+ SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
+
+ // verify the peer's settings were read and applied.
+ assertFalse(connection.peerSettings.getEnablePush(true));
+ }
+
+ @Test public void peerIncreasesMaxFrameSize() throws Exception {
+ int newMaxFrameSize = 0x4001;
+ Settings settings = new Settings();
+ settings.set(Settings.MAX_FRAME_SIZE, 0, newMaxFrameSize);
+
+ SpdyConnection connection = sendHttp2SettingsAndCheckForAck(true, settings);
+
+ // verify the peer's settings were read and applied.
+ assertEquals(newMaxFrameSize, connection.peerSettings.getMaxFrameSize(-1));
+ assertEquals(newMaxFrameSize, connection.frameWriter.maxDataLength());
+ }
+
+ @Test public void receiveGoAwayHttp2() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // write the mocking script
+ peer.acceptFrame(); // SYN_STREAM 3
+ peer.acceptFrame(); // SYN_STREAM 5
+ peer.sendFrame().goAway(3, PROTOCOL_ERROR, Util.EMPTY_BYTE_ARRAY);
+ peer.acceptFrame(); // PING
+ peer.sendFrame().ping(true, 1, 0);
+ peer.acceptFrame(); // DATA STREAM 3
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connection(peer, HTTP_2);
+ SpdyStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
+ SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
+ connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
+ BufferedSink sink1 = Okio.buffer(stream1.getSink());
+ BufferedSink sink2 = Okio.buffer(stream2.getSink());
+ sink1.writeUtf8("abc");
+ try {
+ sink2.writeUtf8("abc");
+ sink2.flush();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("stream was reset: REFUSED_STREAM", expected.getMessage());
+ }
+ sink1.writeUtf8("def");
+ sink1.close();
+ try {
+ connection.newStream(headerEntries("c", "cola"), true, true);
+ fail();
+ } catch (IOException expected) {
+ assertEquals("shutdown", expected.getMessage());
+ }
+ assertTrue(stream1.isOpen());
+ assertFalse(stream2.isOpen());
+ assertEquals(1, connection.openStreamCount());
+
+ // verify the peer received what was expected
+ MockSpdyPeer.InFrame synStream1 = peer.takeFrame();
+ assertEquals(TYPE_HEADERS, synStream1.type);
+ MockSpdyPeer.InFrame synStream2 = peer.takeFrame();
+ assertEquals(TYPE_HEADERS, synStream2.type);
+ MockSpdyPeer.InFrame ping = peer.takeFrame();
+ assertEquals(TYPE_PING, ping.type);
+ MockSpdyPeer.InFrame data1 = peer.takeFrame();
+ assertEquals(TYPE_DATA, data1.type);
+ assertEquals(3, data1.streamId);
+ assertTrue(Arrays.equals("abcdef".getBytes("UTF-8"), data1.data));
+ }
+
+ @Test public void readSendsWindowUpdateHttp2() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ int windowSize = 100;
+ int windowUpdateThreshold = 50;
+
+ // Write the mocking script.
+ peer.acceptFrame(); // SYN_STREAM
+ peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
+ for (int i = 0; i < 3; i++) {
+ // Send frames of summing to size 50, which is windowUpdateThreshold.
+ peer.sendFrame().data(false, 3, data(24), 24);
+ peer.sendFrame().data(false, 3, data(25), 25);
+ peer.sendFrame().data(false, 3, data(1), 1);
+ peer.acceptFrame(); // connection WINDOW UPDATE
+ peer.acceptFrame(); // stream WINDOW UPDATE
+ }
+ peer.sendFrame().data(true, 3, data(0), 0);
+ peer.play();
+
+ // Play it back.
+ SpdyConnection connection = connection(peer, HTTP_2);
+ connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, windowSize);
+ SpdyStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
+ assertEquals(0, stream.unacknowledgedBytesRead);
+ assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
+ Source in = stream.getSource();
+ Buffer buffer = new Buffer();
+ buffer.writeAll(in);
+ assertEquals(-1, in.read(buffer, 1));
+ assertEquals(150, buffer.size());
+
+ MockSpdyPeer.InFrame synStream = peer.takeFrame();
+ assertEquals(TYPE_HEADERS, synStream.type);
+ for (int i = 0; i < 3; i++) {
+ List<Integer> windowUpdateStreamIds = new ArrayList<>(2);
+ for (int j = 0; j < 2; j++) {
+ MockSpdyPeer.InFrame windowUpdate = peer.takeFrame();
+ assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type);
+ windowUpdateStreamIds.add(windowUpdate.streamId);
+ assertEquals(windowUpdateThreshold, windowUpdate.windowSizeIncrement);
+ }
+ assertTrue(windowUpdateStreamIds.contains(0)); // connection
+ assertTrue(windowUpdateStreamIds.contains(3)); // stream
+ }
+ }
+
+ private Buffer data(int byteCount) {
+ return new Buffer().write(new byte[byteCount]);
+ }
+
+ @Test public void serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // Write the mocking script.
+ peer.acceptFrame(); // SYN_STREAM
+ peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
+ peer.sendFrame().data(true, 3, data(0), 0);
+ peer.play();
+
+ // Play it back.
+ SpdyConnection connection = connection(peer, HTTP_2);
+ SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+ assertEquals(-1, client.getSource().read(new Buffer(), 1));
+
+ // Verify the peer received what was expected.
+ MockSpdyPeer.InFrame synStream = peer.takeFrame();
+ assertEquals(TYPE_HEADERS, synStream.type);
+ assertEquals(3, peer.frameCount());
+ }
+
+ @Test public void clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // Write the mocking script.
+ peer.acceptFrame(); // SYN_STREAM
+ peer.acceptFrame(); // DATA
+ peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
+ peer.play();
+
+ // Play it back.
+ SpdyConnection connection = connection(peer, HTTP_2);
+ SpdyStream client = connection.newStream(headerEntries("b", "banana"), true, true);
+ BufferedSink out = Okio.buffer(client.getSink());
+ out.write(Util.EMPTY_BYTE_ARRAY);
+ out.flush();
+ out.close();
+
+ // Verify the peer received what was expected.
+ assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+ assertEquals(TYPE_DATA, peer.takeFrame().type);
+ assertEquals(3, peer.frameCount());
+ }
+
+ @Test public void maxFrameSizeHonored() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ byte[] buff = new byte[peer.maxOutboundDataLength() + 1];
+ Arrays.fill(buff, (byte) '*');
+
+ // write the mocking script
+ peer.acceptFrame(); // SYN_STREAM
+ peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
+ peer.acceptFrame(); // DATA
+ peer.acceptFrame(); // DATA
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connection(peer, HTTP_2);
+ SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+ BufferedSink out = Okio.buffer(stream.getSink());
+ out.write(buff);
+ out.flush();
+ out.close();
+
+ MockSpdyPeer.InFrame synStream = peer.takeFrame();
+ assertEquals(TYPE_HEADERS, synStream.type);
+ MockSpdyPeer.InFrame data = peer.takeFrame();
+ assertEquals(peer.maxOutboundDataLength(), data.data.length);
+ data = peer.takeFrame();
+ assertEquals(1, data.data.length);
+ }
+
+ @Test public void pushPromiseStream() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // write the mocking script
+ peer.acceptFrame(); // SYN_STREAM
+ peer.sendFrame().synReply(false, 3, headerEntries("a", "android"));
+ final List<Header> expectedRequestHeaders = Arrays.asList(
+ new Header(Header.TARGET_METHOD, "GET"),
+ new Header(Header.TARGET_SCHEME, "https"),
+ new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+ new Header(Header.TARGET_PATH, "/cached")
+ );
+ peer.sendFrame().pushPromise(3, 2, expectedRequestHeaders);
+ final List<Header> expectedResponseHeaders = Arrays.asList(
+ new Header(Header.RESPONSE_STATUS, "200")
+ );
+ peer.sendFrame().synReply(true, 2, expectedResponseHeaders);
+ peer.sendFrame().data(true, 3, data(0), 0);
+ peer.play();
+
+ RecordingPushObserver observer = new RecordingPushObserver();
+
+ // play it back
+ SpdyConnection connection = connectionBuilder(peer, HTTP_2)
+ .pushObserver(observer).build();
+ SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
+ assertEquals(-1, client.getSource().read(new Buffer(), 1));
+
+ // verify the peer received what was expected
+ assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+
+ assertEquals(expectedRequestHeaders, observer.takeEvent());
+ assertEquals(expectedResponseHeaders, observer.takeEvent());
+ }
+
+ @Test public void doublePushPromise() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // write the mocking script
+ peer.sendFrame().pushPromise(3, 2, headerEntries("a", "android"));
+ peer.acceptFrame(); // SYN_REPLY
+ peer.sendFrame().pushPromise(3, 2, headerEntries("b", "banana"));
+ peer.acceptFrame(); // RST_STREAM
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connectionBuilder(peer, HTTP_2).build();
+ connection.newStream(headerEntries("b", "banana"), false, true);
+
+ // verify the peer received what was expected
+ assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+ assertEquals(PROTOCOL_ERROR, peer.takeFrame().errorCode);
+ }
+
+ @Test public void pushPromiseStreamsAutomaticallyCancel() throws Exception {
+ peer.setVariantAndClient(HTTP_2, false);
+
+ // write the mocking script
+ peer.sendFrame().pushPromise(3, 2, Arrays.asList(
+ new Header(Header.TARGET_METHOD, "GET"),
+ new Header(Header.TARGET_SCHEME, "https"),
+ new Header(Header.TARGET_AUTHORITY, "squareup.com"),
+ new Header(Header.TARGET_PATH, "/cached")
+ ));
+ peer.sendFrame().synReply(true, 2, Arrays.asList(
+ new Header(Header.RESPONSE_STATUS, "200")
+ ));
+ peer.acceptFrame(); // RST_STREAM
+ peer.play();
+
+ // play it back
+ connectionBuilder(peer, HTTP_2)
+ .pushObserver(PushObserver.CANCEL).build();
+
+ // verify the peer received what was expected
+ MockSpdyPeer.InFrame rstStream = peer.takeFrame();
+ assertEquals(TYPE_RST_STREAM, rstStream.type);
+ assertEquals(2, rstStream.streamId);
+ assertEquals(CANCEL, rstStream.errorCode);
+ }
+
+ private SpdyConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
+ throws IOException, InterruptedException {
+ peer.setVariantAndClient(HTTP_2, client);
+ peer.sendFrame().settings(settings);
+ peer.acceptFrame(); // ACK
+ peer.acceptFrame(); // PING
+ peer.sendFrame().ping(true, 1, 0);
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connection(peer, HTTP_2);
+
+ // verify the peer received the ACK
+ MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
+ assertEquals(TYPE_SETTINGS, ackFrame.type);
+ assertEquals(0, ackFrame.streamId);
+ assertTrue(ackFrame.ack);
+
+ connection.ping().roundTripTime(); // Ensure that settings have been applied before returning.
+ return connection;
+ }
+
+ private SpdyConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
+ return connectionBuilder(peer, variant).build();
+ }
+
+ private SpdyConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
+ throws IOException {
+ return new SpdyConnection.Builder(true, peer.openSocket())
+ .pushObserver(IGNORE)
+ .protocol(variant.getProtocol());
+ }
+
+ static final PushObserver IGNORE = new PushObserver() {
+
+ @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
+ return false;
+ }
+
+ @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
+ return false;
+ }
+
+ @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
+ boolean last) throws IOException {
+ source.skip(byteCount);
+ return false;
+ }
+
+ @Override public void onReset(int streamId, ErrorCode errorCode) {
+ }
+ };
+
+ private static class RecordingPushObserver implements PushObserver {
+ final List<Object> events = new ArrayList<>();
+
+ public synchronized Object takeEvent() throws InterruptedException {
+ while (events.isEmpty()) {
+ wait();
+ }
+ return events.remove(0);
+ }
+
+ @Override public synchronized boolean onRequest(int streamId, List<Header> requestHeaders) {
+ assertEquals(2, streamId);
+ events.add(requestHeaders);
+ notifyAll();
+ return false;
+ }
+
+ @Override public synchronized boolean onHeaders(
+ int streamId, List<Header> responseHeaders, boolean last) {
+ assertEquals(2, streamId);
+ assertTrue(last);
+ events.add(responseHeaders);
+ notifyAll();
+ return false;
+ }
+
+ @Override public synchronized boolean onData(
+ int streamId, BufferedSource source, int byteCount, boolean last) {
+ events.add(new AssertionError("onData"));
+ notifyAll();
+ return false;
+ }
+
+ @Override public synchronized void onReset(int streamId, ErrorCode errorCode) {
+ events.add(new AssertionError("onReset"));
+ notifyAll();
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
index 6206b7e..222d23e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/HuffmanTest.java
@@ -43,18 +43,13 @@
}
private void assertRoundTrip(byte[] buf) throws IOException {
- assertRoundTrip(Huffman.Codec.REQUEST, buf);
- assertRoundTrip(Huffman.Codec.RESPONSE, buf);
- }
-
- private static void assertRoundTrip(Huffman.Codec codec, byte[] buf) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
- codec.encode(buf, dos);
- assertEquals(baos.size(), codec.encodedLength(buf));
+ Huffman.get().encode(buf, dos);
+ assertEquals(baos.size(), Huffman.get().encodedLength(buf));
- byte[] decodedBytes = codec.decode(baos.toByteArray());
+ byte[] decodedBytes = Huffman.get().decode(baos.toByteArray());
assertTrue(Arrays.equals(buf, decodedBytes));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
index fd6007d..fd6da2b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/MockSpdyPeer.java
@@ -30,9 +30,9 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
+import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
-import okio.OkBuffer;
import okio.Okio;
/** Replays prerecorded outgoing frames and records incoming frames. */
@@ -40,10 +40,10 @@
private int frameCount = 0;
private boolean client = false;
private Variant variant = new Spdy3();
- private final OkBuffer bytesOut = new OkBuffer();
+ private final Buffer bytesOut = new Buffer();
private FrameWriter frameWriter = variant.newWriter(bytesOut, client);
- private final List<OutFrame> outFrames = new ArrayList<OutFrame>();
- private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<InFrame>();
+ private final List<OutFrame> outFrames = new ArrayList<>();
+ private final BlockingQueue<InFrame> inFrames = new LinkedBlockingQueue<>();
private int port;
private final ExecutorService executor = Executors.newSingleThreadExecutor(
Util.threadFactory("MockSpdyPeer", false));
@@ -63,13 +63,18 @@
frameCount++;
}
+ /** Maximum length of an outbound data frame. */
+ public int maxOutboundDataLength() {
+ return frameWriter.maxDataLength();
+ }
+
/** Count of frames sent or received. */
public int frameCount() {
return frameCount;
}
public FrameWriter sendFrame() {
- outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE));
+ outFrames.add(new OutFrame(frameCount++, bytesOut.size(), false));
return frameWriter;
}
@@ -78,17 +83,27 @@
* won't be generated naturally.
*/
public void sendFrame(byte[] frame) throws IOException {
- outFrames.add(new OutFrame(frameCount++, bytesOut.size(), Integer.MAX_VALUE));
+ outFrames.add(new OutFrame(frameCount++, bytesOut.size(), false));
bytesOut.write(frame);
}
/**
- * Sends a frame, truncated to {@code truncateToLength} bytes. This is only
- * useful for testing error handling as the truncated frame will be
- * malformed.
+ * Shortens the last frame from its original length to {@code length}. This
+ * will cause the peer to close the socket as soon as this frame has been
+ * written; otherwise the peer stays open until explicitly closed.
*/
- public FrameWriter sendTruncatedFrame(int truncateToLength) {
- outFrames.add(new OutFrame(frameCount++, bytesOut.size(), truncateToLength));
+ public FrameWriter truncateLastFrame(int length) {
+ OutFrame lastFrame = outFrames.remove(outFrames.size() - 1);
+ if (length >= bytesOut.size() - lastFrame.start) throw new IllegalArgumentException();
+
+ // Move everything from bytesOut into a new buffer.
+ Buffer fullBuffer = new Buffer();
+ bytesOut.read(fullBuffer, bytesOut.size());
+
+ // Copy back all but what we're truncating.
+ fullBuffer.read(bytesOut, lastFrame.start + length);
+
+ outFrames.add(new OutFrame(lastFrame.sequence, lastFrame.start, true));
return frameWriter;
}
@@ -107,7 +122,7 @@
readAndWriteFrames();
} catch (IOException e) {
Util.closeQuietly(MockSpdyPeer.this);
- throw new RuntimeException(e);
+ e.printStackTrace();
}
}
});
@@ -121,7 +136,7 @@
FrameReader reader = variant.newReader(Okio.buffer(Okio.source(in)), client);
Iterator<OutFrame> outFramesIterator = outFrames.iterator();
- byte[] outBytes = bytesOut.readByteString(bytesOut.size()).toByteArray();
+ byte[] outBytes = bytesOut.readByteArray();
OutFrame nextOutFrame = null;
for (int i = 0; i < frameCount; i++) {
@@ -131,18 +146,25 @@
if (nextOutFrame != null && nextOutFrame.sequence == i) {
long start = nextOutFrame.start;
- int truncateToLength = nextOutFrame.truncateToLength;
+ boolean truncated;
long end;
if (outFramesIterator.hasNext()) {
nextOutFrame = outFramesIterator.next();
end = nextOutFrame.start;
+ truncated = false;
} else {
end = outBytes.length;
+ truncated = nextOutFrame.truncated;
}
- // write a frame
- int length = (int) Math.min(end - start, truncateToLength);
+ // Write a frame.
+ int length = (int) (end - start);
out.write(outBytes, (int) start, length);
+
+ // If the last frame was truncated, immediately close the connection.
+ if (truncated) {
+ socket.close();
+ }
} else {
// read a frame
InFrame inFrame = new InFrame(i, reader);
@@ -150,7 +172,6 @@
inFrames.add(inFrame);
}
}
- Util.closeQuietly(socket);
}
public Socket openSocket() throws IOException {
@@ -174,12 +195,12 @@
private static class OutFrame {
private final int sequence;
private final long start;
- private final int truncateToLength;
+ private final boolean truncated;
- private OutFrame(int sequence, long start, int truncateToLength) {
+ private OutFrame(int sequence, long start, boolean truncated) {
this.sequence = sequence;
this.start = start;
- this.truncateToLength = truncateToLength;
+ this.truncated = truncated;
}
}
@@ -192,7 +213,6 @@
public boolean inFinished;
public int streamId;
public int associatedStreamId;
- public int priority;
public ErrorCode errorCode;
public long windowSizeIncrement;
public List<Header> headerBlock;
@@ -222,15 +242,13 @@
}
@Override public void headers(boolean outFinished, boolean inFinished, int streamId,
- int associatedStreamId, int priority, List<Header> headerBlock,
- HeadersMode headersMode) {
+ int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
if (this.type != -1) throw new IllegalStateException();
this.type = Spdy3.TYPE_HEADERS;
this.outFinished = outFinished;
this.inFinished = inFinished;
this.streamId = streamId;
this.associatedStreamId = associatedStreamId;
- this.priority = priority;
this.headerBlock = headerBlock;
this.headersMode = headersMode;
}
@@ -274,16 +292,22 @@
this.windowSizeIncrement = windowSizeIncrement;
}
- @Override public void priority(int streamId, int priority) {
+ @Override public void priority(int streamId, int streamDependency, int weight,
+ boolean exclusive) {
throw new UnsupportedOperationException();
}
@Override
public void pushPromise(int streamId, int associatedStreamId, List<Header> headerBlock) {
- this.type = Http20Draft09.TYPE_PUSH_PROMISE;
+ this.type = Http20Draft16.TYPE_PUSH_PROMISE;
this.streamId = streamId;
this.associatedStreamId = associatedStreamId;
this.headerBlock = headerBlock;
}
+
+ @Override public void alternateService(int streamId, String origin, ByteString protocol,
+ String host, int port, long maxAge) {
+ throw new UnsupportedOperationException();
+ }
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
index 294684f..f9f9efa 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SettingsTest.java
@@ -37,7 +37,7 @@
@Test public void setFields() {
Settings settings = new Settings();
- // WARNING: clash on flags between spdy/3 and http/2!
+ // WARNING: clash on flags between spdy/3 and HTTP/2!
assertEquals(-3, settings.getUploadBandwidth(-3));
assertEquals(-1, settings.getHeaderTableSize());
settings.set(UPLOAD_BANDWIDTH, 0, 42);
@@ -45,13 +45,14 @@
settings.set(Settings.HEADER_TABLE_SIZE, 0, 8096);
assertEquals(8096, settings.getHeaderTableSize());
- // WARNING: clash on flags between spdy/3 and http/2!
- assertEquals(-3, settings.getDownloadBandwidth(-3));
+ // WARNING: clash on flags between spdy/3 and HTTP/2!
assertEquals(true, settings.getEnablePush(true));
+ settings.set(Settings.ENABLE_PUSH, 0, 1);
+ assertEquals(true, settings.getEnablePush(false));
+ settings.clear();
+ assertEquals(-3, settings.getDownloadBandwidth(-3));
settings.set(DOWNLOAD_BANDWIDTH, 0, 53);
assertEquals(53, settings.getDownloadBandwidth(-3));
- settings.set(Settings.ENABLE_PUSH, 0, 0);
- assertEquals(false, settings.getEnablePush(true));
assertEquals(-3, settings.getRoundTripTime(-3));
settings.set(Settings.ROUND_TRIP_TIME, 0, 64);
@@ -61,13 +62,23 @@
settings.set(MAX_CONCURRENT_STREAMS, 0, 75);
assertEquals(75, settings.getMaxConcurrentStreams(-3));
+ // WARNING: clash on flags between spdy/3 and HTTP/2!
assertEquals(-3, settings.getCurrentCwnd(-3));
settings.set(Settings.CURRENT_CWND, 0, 86);
assertEquals(86, settings.getCurrentCwnd(-3));
+ settings.clear();
+ assertEquals(16384, settings.getMaxFrameSize(16384));
+ settings.set(Settings.MAX_FRAME_SIZE, 0, 16777215);
+ assertEquals(16777215, settings.getMaxFrameSize(16384));
+ // WARNING: clash on flags between spdy/3 and HTTP/2!
assertEquals(-3, settings.getDownloadRetransRate(-3));
settings.set(DOWNLOAD_RETRANS_RATE, 0, 97);
assertEquals(97, settings.getDownloadRetransRate(-3));
+ settings.clear();
+ assertEquals(-1, settings.getMaxHeaderListSize(-1));
+ settings.set(Settings.MAX_HEADER_LIST_SIZE, 0, 16777215);
+ assertEquals(16777215, settings.getMaxHeaderListSize(-1));
assertEquals(DEFAULT_INITIAL_WINDOW_SIZE,
settings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3ConnectionTest.java
similarity index 72%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
rename to okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3ConnectionTest.java
index 2ef127e..110412e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/SpdyConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3ConnectionTest.java
@@ -23,16 +23,16 @@
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import okio.Buffer;
import okio.BufferedSink;
-import okio.BufferedSource;
import okio.ByteString;
-import okio.OkBuffer;
import okio.Okio;
+import okio.Sink;
import okio.Source;
import org.junit.After;
import org.junit.Test;
-import static com.squareup.okhttp.internal.Util.headerEntries;
+import static com.squareup.okhttp.TestUtil.headerEntries;
import static com.squareup.okhttp.internal.spdy.ErrorCode.CANCEL;
import static com.squareup.okhttp.internal.spdy.ErrorCode.INTERNAL_ERROR;
import static com.squareup.okhttp.internal.spdy.ErrorCode.INVALID_STREAM;
@@ -46,16 +46,14 @@
import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_HEADERS;
import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_PING;
import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_RST_STREAM;
-import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_SETTINGS;
import static com.squareup.okhttp.internal.spdy.Spdy3.TYPE_WINDOW_UPDATE;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-public final class SpdyConnectionTest {
+public final class Spdy3ConnectionTest {
private static final Variant SPDY3 = new Spdy3();
- private static final Variant HTTP_20_DRAFT_09 = new Http20Draft09();
private final MockSpdyPeer peer = new MockSpdyPeer();
@After public void tearDown() throws Exception {
@@ -67,7 +65,7 @@
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame()
.synReply(false, 1, headerEntries("a", "android"));
- peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("robot"));
+ peer.sendFrame().data(true, 1, new Buffer().writeUtf8("robot"), 5);
peer.acceptFrame(); // DATA
peer.play();
@@ -97,12 +95,15 @@
@Test public void headersOnlyStreamIsClosedAfterReplyHeaders() throws Exception {
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
+ peer.acceptFrame(); // PING
+ peer.sendFrame().ping(true, 1, 0);
peer.play();
SpdyConnection connection = connection(peer, SPDY3);
SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, false);
assertEquals(1, connection.openStreamCount());
assertEquals(headerEntries("b", "banana"), stream.getResponseHeaders());
+ connection.ping().roundTripTime(); // Ensure that inFinished has been received.
assertEquals(0, connection.openStreamCount());
}
@@ -139,7 +140,7 @@
":version", "HTTP/1.1",
"content-type", "text/html");
// write the mocking script
- peer.sendFrame().synStream(false, false, 2, 0, 5, 129, pushHeaders);
+ peer.sendFrame().synStream(false, false, 2, 0, pushHeaders);
peer.acceptFrame(); // SYN_REPLY
peer.play();
@@ -150,7 +151,6 @@
receiveCount.incrementAndGet();
assertEquals(pushHeaders, stream.getRequestHeaders());
assertEquals(null, stream.getErrorCode());
- assertEquals(5, stream.getPriority());
stream.reply(headerEntries("b", "banana"), true);
}
};
@@ -168,7 +168,7 @@
@Test public void replyWithNoData() throws Exception {
// write the mocking script
- peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("a", "android"));
+ peer.sendFrame().synStream(false, false, 2, 0, headerEntries("a", "android"));
peer.acceptFrame(); // SYN_REPLY
peer.play();
@@ -209,26 +209,6 @@
assertTrue(ping.ack);
}
- @Test public void serverPingsClientHttp2() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- // write the mocking script
- peer.sendFrame().ping(false, 2, 3);
- peer.acceptFrame(); // PING
- peer.play();
-
- // play it back
- connection(peer, HTTP_20_DRAFT_09);
-
- // verify the peer received what was expected
- MockSpdyPeer.InFrame ping = peer.takeFrame();
- assertEquals(TYPE_PING, ping.type);
- assertEquals(0, ping.streamId);
- assertEquals(2, ping.payload1);
- assertEquals(3, ping.payload2);
- assertTrue(ping.ack);
- }
-
@Test public void clientPingsServer() throws Exception {
// write the mocking script
peer.acceptFrame(); // PING
@@ -249,64 +229,6 @@
assertFalse(pingFrame.ack);
}
- @Test public void clientPingsServerHttp2() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- // write the mocking script
- peer.acceptFrame(); // PING
- peer.sendFrame().ping(true, 1, 5);
- peer.play();
-
- // play it back
- SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
- Ping ping = connection.ping();
- assertTrue(ping.roundTripTime() > 0);
- assertTrue(ping.roundTripTime() < TimeUnit.SECONDS.toNanos(1));
-
- // verify the peer received what was expected
- MockSpdyPeer.InFrame pingFrame = peer.takeFrame();
- assertEquals(0, pingFrame.streamId);
- assertEquals(1, pingFrame.payload1);
- assertEquals(0x4f4b6f6b, pingFrame.payload2); // connection.ping() sets this.
- assertFalse(pingFrame.ack);
- }
-
- @Test public void peerHttp2ServerLowersInitialWindowSize() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- Settings initial = new Settings();
- initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 1684);
- Settings shouldntImpactConnection = new Settings();
- shouldntImpactConnection.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 3368);
-
- peer.sendFrame().settings(initial);
- peer.acceptFrame(); // ACK
- peer.sendFrame().settings(shouldntImpactConnection);
- peer.acceptFrame(); // ACK 2
- peer.acceptFrame(); // HEADERS
- peer.play();
-
- SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
-
- // verify the peer received the ACK
- MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
- assertEquals(TYPE_SETTINGS, ackFrame.type);
- assertEquals(0, ackFrame.streamId);
- assertTrue(ackFrame.ack);
- ackFrame = peer.takeFrame();
- assertEquals(TYPE_SETTINGS, ackFrame.type);
- assertEquals(0, ackFrame.streamId);
- assertTrue(ackFrame.ack);
-
- // This stream was created *after* the connection settings were adjusted.
- SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
-
- assertEquals(3368, connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE));
- assertEquals(1684, connection.bytesLeftInWriteWindow); // initial wasn't affected.
- // New Stream is has the most recent initial window size.
- assertEquals(3368, stream.bytesLeftInWriteWindow);
- }
-
@Test public void unexpectedPingIsNotReturned() throws Exception {
// write the mocking script
peer.sendFrame().ping(false, 2, 0);
@@ -326,35 +248,6 @@
assertEquals(4, ping4.payload1);
}
- @Test public void peerHttp2ServerZerosCompressionTable() throws Exception {
- boolean client = false; // Peer is server, so we are client.
- Settings settings = new Settings();
- settings.set(Settings.HEADER_TABLE_SIZE, PERSIST_VALUE, 0);
-
- SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
-
- // verify the peer's settings were read and applied.
- synchronized (connection) {
- assertEquals(0, connection.peerSettings.getHeaderTableSize());
- Http20Draft09.Reader frameReader = (Http20Draft09.Reader) connection.frameReader;
- assertEquals(0, frameReader.hpackReader.maxHeaderTableByteCount());
- // TODO: when supported, check the frameWriter's compression table is unaffected.
- }
- }
-
- @Test public void peerHttp2ClientDisablesPush() throws Exception {
- boolean client = false; // Peer is client, so we are server.
- Settings settings = new Settings();
- settings.set(Settings.ENABLE_PUSH, 0, 0); // The peer client disables push.
-
- SpdyConnection connection = sendHttp2SettingsAndCheckForAck(client, settings);
-
- // verify the peer's settings were read and applied.
- synchronized (connection) {
- assertFalse(connection.peerSettings.getEnablePush(true));
- }
- }
-
@Test public void serverSendsSettingsToClient() throws Exception {
// write the mocking script
Settings settings = new Settings();
@@ -436,7 +329,7 @@
@Test public void bogusDataFrameDoesNotDisruptConnection() throws Exception {
// write the mocking script
- peer.sendFrame().data(true, 41, new OkBuffer().writeUtf8("bogus"));
+ peer.sendFrame().data(true, 41, new Buffer().writeUtf8("bogus"), 5);
peer.acceptFrame(); // RST_STREAM
peer.sendFrame().ping(false, 2, 0);
peer.acceptFrame(); // PING
@@ -575,7 +468,7 @@
BufferedSink out = Okio.buffer(stream.getSink());
in.close();
try {
- in.read(new OkBuffer(), 1);
+ in.read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream closed", expected.getMessage());
@@ -619,7 +512,7 @@
BufferedSink out = Okio.buffer(stream.getSink());
source.close();
try {
- source.read(new OkBuffer(), 1);
+ source.read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream closed", expected.getMessage());
@@ -651,7 +544,9 @@
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
- peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("square"));
+ peer.sendFrame().data(true, 1, new Buffer().writeUtf8("square"), 6);
+ peer.acceptFrame(); // PING
+ peer.sendFrame().ping(true, 1, 0);
peer.play();
// play it back
@@ -659,6 +554,7 @@
SpdyStream stream = connection.newStream(headerEntries("a", "android"), false, true);
Source source = stream.getSource();
assertStreamData("square", source);
+ connection.ping().roundTripTime(); // Ensure that inFinished has been received.
assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
@@ -685,7 +581,7 @@
assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
connection.ping().roundTripTime(); // Ensure that the 2nd SYN REPLY has been received.
try {
- stream.getSource().read(new OkBuffer(), 1);
+ stream.getSource().read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream was reset: STREAM_IN_USE", expected.getMessage());
@@ -705,9 +601,9 @@
@Test public void remoteDoubleSynStream() throws Exception {
// write the mocking script
- peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("a", "android"));
+ peer.sendFrame().synStream(false, false, 2, 0, headerEntries("a", "android"));
peer.acceptFrame(); // SYN_REPLY
- peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("b", "banana"));
+ peer.sendFrame().synStream(false, false, 2, 0, headerEntries("b", "banana"));
peer.acceptFrame(); // RST_STREAM
peer.play();
@@ -738,8 +634,8 @@
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
- peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("robot"));
- peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("c3po")); // Ignored.
+ peer.sendFrame().data(true, 1, new Buffer().writeUtf8("robot"), 5);
+ peer.sendFrame().data(true, 1, new Buffer().writeUtf8("c3po"), 4); // Ignored.
peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
peer.acceptFrame(); // PING
peer.play();
@@ -760,10 +656,11 @@
}
@Test public void clientDoesNotLimitFlowControl() throws Exception {
+ int dataLength = 64 * 1024 + 1;
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("b", "banana"));
- peer.sendFrame().data(false, 1, new OkBuffer().write(new byte[64 * 1024 + 1]));
+ peer.sendFrame().data(false, 1, new Buffer().write(new byte[dataLength]), dataLength);
peer.sendFrame().ping(false, 2, 0); // Ping just to make sure the stream was fastforwarded.
peer.acceptFrame(); // PING
peer.play();
@@ -812,15 +709,7 @@
@Test public void receiveGoAway() throws Exception {
- receiveGoAway(SPDY3);
- }
-
- @Test public void receiveGoAwayHttp2() throws Exception {
- receiveGoAway(HTTP_20_DRAFT_09);
- }
-
- private void receiveGoAway(Variant variant) throws Exception {
- peer.setVariantAndClient(variant, false);
+ peer.setVariantAndClient(SPDY3, false);
// write the mocking script
peer.acceptFrame(); // SYN_STREAM 1
@@ -832,10 +721,10 @@
peer.play();
// play it back
- SpdyConnection connection = connection(peer, variant);
+ SpdyConnection connection = connection(peer, SPDY3);
SpdyStream stream1 = connection.newStream(headerEntries("a", "android"), true, true);
SpdyStream stream2 = connection.newStream(headerEntries("b", "banana"), true, true);
- connection.ping().roundTripTime(); // Ensure that the GO_AWAY has been received.
+ connection.ping().roundTripTime(); // Ensure the GO_AWAY that resets stream2 has been received.
BufferedSink sink1 = Okio.buffer(stream1.getSink());
BufferedSink sink2 = Okio.buffer(stream2.getSink());
sink1.writeUtf8("abc");
@@ -854,6 +743,8 @@
} catch (IOException expected) {
assertEquals("shutdown", expected.getMessage());
}
+ assertTrue(stream1.isOpen());
+ assertFalse(stream2.isOpen());
assertEquals(1, connection.openStreamCount());
// verify the peer received what was expected
@@ -874,7 +765,7 @@
peer.acceptFrame(); // SYN_STREAM 1
peer.acceptFrame(); // GOAWAY
peer.acceptFrame(); // PING
- peer.sendFrame().synStream(false, false, 2, 0, 0, 0, headerEntries("b", "b")); // Should be ignored!
+ peer.sendFrame().synStream(false, false, 2, 0, headerEntries("b", "b")); // Should be ignored!
peer.sendFrame().ping(true, 1, 0);
peer.play();
@@ -946,7 +837,7 @@
assertEquals("stream was reset: CANCEL", expected.getMessage());
}
try {
- stream.getSource().read(new OkBuffer(), 1);
+ stream.getSource().read(new Buffer(), 1);
fail();
} catch (IOException expected) {
assertEquals("stream was reset: CANCEL", expected.getMessage());
@@ -976,33 +867,94 @@
assertEquals(-1, ping.roundTripTime());
}
- @Test public void readTimeoutExpires() throws Exception {
+ @Test public void getResponseHeadersTimesOut() throws Exception {
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
- peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
- peer.acceptFrame(); // PING
- peer.sendFrame().ping(true, 1, 0);
+ peer.acceptFrame(); // RST_STREAM
peer.play();
// play it back
SpdyConnection connection = connection(peer, SPDY3);
SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
- stream.setReadTimeout(1000);
+ stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS);
+ long startNanos = System.nanoTime();
+ try {
+ stream.getResponseHeaders();
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ long elapsedNanos = System.nanoTime() - startNanos;
+ assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
+ assertEquals(0, connection.openStreamCount());
+
+ // verify the peer received what was expected
+ assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+ assertEquals(TYPE_RST_STREAM, peer.takeFrame().type);
+ }
+
+ @Test public void readTimesOut() throws Exception {
+ // write the mocking script
+ peer.acceptFrame(); // SYN_STREAM
+ peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+ peer.acceptFrame(); // RST_STREAM
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connection(peer, SPDY3);
+ SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+ stream.readTimeout().timeout(500, TimeUnit.MILLISECONDS);
Source source = stream.getSource();
long startNanos = System.nanoTime();
try {
- source.read(new OkBuffer(), 1);
+ source.read(new Buffer(), 1);
fail();
- } catch (IOException expected) {
+ } catch (InterruptedIOException expected) {
}
long elapsedNanos = System.nanoTime() - startNanos;
- assertEquals(1000d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
- assertEquals(1, connection.openStreamCount());
- connection.ping().roundTripTime(); // Prevent the peer from exiting prematurely.
+ assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
+ assertEquals(0, connection.openStreamCount());
// verify the peer received what was expected
- MockSpdyPeer.InFrame synStream = peer.takeFrame();
- assertEquals(TYPE_HEADERS, synStream.type);
+ assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+ assertEquals(TYPE_RST_STREAM, peer.takeFrame().type);
+ }
+
+ @Test public void writeTimesOutAwaitingStreamWindow() throws Exception {
+ // Set the peer's receive window to 5 bytes!
+ Settings peerSettings = new Settings().set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, 5);
+
+ // write the mocking script
+ peer.sendFrame().settings(peerSettings);
+ peer.acceptFrame(); // PING
+ peer.sendFrame().ping(true, 1, 0);
+ peer.acceptFrame(); // SYN_STREAM
+ peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
+ peer.acceptFrame(); // DATA
+ peer.acceptFrame(); // RST_STREAM
+ peer.play();
+
+ // play it back
+ SpdyConnection connection = connection(peer, SPDY3);
+ connection.ping().roundTripTime(); // Make sure settings have been received.
+ SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
+ Sink sink = stream.getSink();
+ sink.write(new Buffer().writeUtf8("abcde"), 5);
+ stream.writeTimeout().timeout(500, TimeUnit.MILLISECONDS);
+ long startNanos = System.nanoTime();
+ try {
+ sink.write(new Buffer().writeUtf8("f"), 1); // This will time out waiting on the write window.
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ long elapsedNanos = System.nanoTime() - startNanos;
+ assertEquals(500d, TimeUnit.NANOSECONDS.toMillis(elapsedNanos), 200d /* 200ms delta */);
+ assertEquals(0, connection.openStreamCount());
+
+ // verify the peer received what was expected
+ assertEquals(TYPE_PING, peer.takeFrame().type);
+ assertEquals(TYPE_HEADERS, peer.takeFrame().type);
+ assertEquals(TYPE_DATA, peer.takeFrame().type);
+ assertEquals(TYPE_RST_STREAM, peer.takeFrame().type);
}
@Test public void headers() throws Exception {
@@ -1060,50 +1012,41 @@
}
@Test public void readSendsWindowUpdate() throws Exception {
- readSendsWindowUpdate(SPDY3);
- }
+ peer.setVariantAndClient(SPDY3, false);
- @Test public void readSendsWindowUpdateHttp2() throws Exception {
- readSendsWindowUpdate(HTTP_20_DRAFT_09);
- }
-
- private void readSendsWindowUpdate(Variant variant)
- throws IOException, InterruptedException {
- peer.setVariantAndClient(variant, false);
-
- int windowUpdateThreshold = DEFAULT_INITIAL_WINDOW_SIZE / 2;
+ int windowSize = 100;
+ int windowUpdateThreshold = 50;
// Write the mocking script.
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
for (int i = 0; i < 3; i++) {
- // Send frames summing to windowUpdateThreshold.
- for (int sent = 0, count; sent < windowUpdateThreshold; sent += count) {
- count = Math.min(variant.maxFrameSize(), windowUpdateThreshold - sent);
- peer.sendFrame().data(false, 1, data(count));
- }
+ // Send frames of summing to size 50, which is windowUpdateThreshold.
+ peer.sendFrame().data(false, 1, data(24), 24);
+ peer.sendFrame().data(false, 1, data(25), 25);
+ peer.sendFrame().data(false, 1, data(1), 1);
peer.acceptFrame(); // connection WINDOW UPDATE
peer.acceptFrame(); // stream WINDOW UPDATE
}
- peer.sendFrame().data(true, 1, data(0));
+ peer.sendFrame().data(true, 1, data(0), 0);
peer.play();
// Play it back.
- SpdyConnection connection = connection(peer, variant);
+ SpdyConnection connection = connection(peer, SPDY3);
+ connection.okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, windowSize);
SpdyStream stream = connection.newStream(headerEntries("b", "banana"), false, true);
assertEquals(0, stream.unacknowledgedBytesRead);
assertEquals(headerEntries("a", "android"), stream.getResponseHeaders());
Source in = stream.getSource();
- OkBuffer buffer = new OkBuffer();
- while (in.read(buffer, 1024) != -1) {
- if (buffer.size() == 3 * windowUpdateThreshold) break;
- }
+ Buffer buffer = new Buffer();
+ buffer.writeAll(in);
assertEquals(-1, in.read(buffer, 1));
+ assertEquals(150, buffer.size());
MockSpdyPeer.InFrame synStream = peer.takeFrame();
assertEquals(TYPE_HEADERS, synStream.type);
for (int i = 0; i < 3; i++) {
- List<Integer> windowUpdateStreamIds = new ArrayList(2);
+ List<Integer> windowUpdateStreamIds = new ArrayList<>(2);
for (int j = 0; j < 2; j++) {
MockSpdyPeer.InFrame windowUpdate = peer.takeFrame();
assertEquals(TYPE_WINDOW_UPDATE, windowUpdate.type);
@@ -1115,32 +1058,23 @@
}
}
- private OkBuffer data(int byteCount) {
- return new OkBuffer().write(new byte[byteCount]);
+ private Buffer data(int byteCount) {
+ return new Buffer().write(new byte[byteCount]);
}
@Test public void serverSendsEmptyDataClientDoesntSendWindowUpdate() throws Exception {
- serverSendsEmptyDataClientDoesntSendWindowUpdate(SPDY3);
- }
-
- @Test public void serverSendsEmptyDataClientDoesntSendWindowUpdateHttp2() throws Exception {
- serverSendsEmptyDataClientDoesntSendWindowUpdate(HTTP_20_DRAFT_09);
- }
-
- private void serverSendsEmptyDataClientDoesntSendWindowUpdate(Variant variant)
- throws IOException, InterruptedException {
- peer.setVariantAndClient(variant, false);
+ peer.setVariantAndClient(SPDY3, false);
// Write the mocking script.
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
- peer.sendFrame().data(true, 1, data(0));
+ peer.sendFrame().data(true, 1, data(0), 0);
peer.play();
// Play it back.
- SpdyConnection connection = connection(peer, variant);
+ SpdyConnection connection = connection(peer, SPDY3);
SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
- assertEquals(-1, client.getSource().read(new OkBuffer(), 1));
+ assertEquals(-1, client.getSource().read(new Buffer(), 1));
// Verify the peer received what was expected.
MockSpdyPeer.InFrame synStream = peer.takeFrame();
@@ -1149,16 +1083,7 @@
}
@Test public void clientSendsEmptyDataServerDoesntSendWindowUpdate() throws Exception {
- clientSendsEmptyDataServerDoesntSendWindowUpdate(SPDY3);
- }
-
- @Test public void clientSendsEmptyDataServerDoesntSendWindowUpdateHttp2() throws Exception {
- clientSendsEmptyDataServerDoesntSendWindowUpdate(HTTP_20_DRAFT_09);
- }
-
- private void clientSendsEmptyDataServerDoesntSendWindowUpdate(Variant variant)
- throws IOException, InterruptedException {
- peer.setVariantAndClient(variant, false);
+ peer.setVariantAndClient(SPDY3, false);
// Write the mocking script.
peer.acceptFrame(); // SYN_STREAM
@@ -1167,7 +1092,7 @@
peer.play();
// Play it back.
- SpdyConnection connection = connection(peer, variant);
+ SpdyConnection connection = connection(peer, SPDY3);
SpdyStream client = connection.newStream(headerEntries("b", "banana"), true, true);
BufferedSink out = Okio.buffer(client.getSink());
out.write(Util.EMPTY_BYTE_ARRAY);
@@ -1180,102 +1105,12 @@
assertEquals(3, peer.frameCount());
}
- @Test public void writeAwaitsWindowUpdate() throws Exception {
- int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
-
- // Write the mocking script. This accepts more data frames than necessary!
- peer.acceptFrame(); // SYN_STREAM
- for (int i = 0; i < framesThatFillWindow; i++) {
- peer.acceptFrame(); // DATA
- }
- peer.acceptFrame(); // DATA we won't be able to flush until a window update.
- peer.play();
-
- // Play it back.
- SpdyConnection connection = connection(peer, SPDY3);
- SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
- BufferedSink out = Okio.buffer(stream.getSink());
- out.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
- out.flush();
-
- // Check that we've filled the window for both the stream and also the connection.
- assertEquals(0, connection.bytesLeftInWriteWindow);
- assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
-
- out.writeByte('a');
- assertFlushBlocks(out);
-
- // receiving a window update on the connection isn't enough.
- connection.readerRunnable.windowUpdate(0, 1);
- assertFlushBlocks(out);
-
- // receiving a window update on the stream will unblock the stream.
- connection.readerRunnable.windowUpdate(1, 1);
- out.flush();
-
- // Verify the peer received what was expected.
- MockSpdyPeer.InFrame synStream = peer.takeFrame();
- assertEquals(TYPE_HEADERS, synStream.type);
- for (int i = 0; i < framesThatFillWindow; i++) {
- MockSpdyPeer.InFrame data = peer.takeFrame();
- assertEquals(TYPE_DATA, data.type);
- }
- }
-
- @Test public void initialSettingsWithWindowSizeAdjustsConnection() throws Exception {
- int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, HTTP_20_DRAFT_09.maxFrameSize());
-
- // Write the mocking script. This accepts more data frames than necessary!
- peer.acceptFrame(); // SYN_STREAM
- for (int i = 0; i < framesThatFillWindow; i++) {
- peer.acceptFrame(); // DATA on stream 1
- }
- peer.acceptFrame(); // DATA on stream 2
- peer.play();
-
- // Play it back.
- SpdyConnection connection = connection(peer, SPDY3);
- SpdyStream stream = connection.newStream(headerEntries("a", "apple"), true, true);
- BufferedSink out = Okio.buffer(stream.getSink());
- out.write(new byte[DEFAULT_INITIAL_WINDOW_SIZE]);
- out.flush();
-
- // write 1 more than the window size
- out.writeByte('a');
- assertFlushBlocks(out);
-
- // Check that we've filled the window for both the stream and also the connection.
- assertEquals(0, connection.bytesLeftInWriteWindow);
- assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
-
- // Receiving a Settings with a larger window size will unblock the streams.
- Settings initial = new Settings();
- initial.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, DEFAULT_INITIAL_WINDOW_SIZE + 1);
- connection.readerRunnable.settings(false, initial);
-
- assertEquals(1, connection.bytesLeftInWriteWindow);
- assertEquals(1, connection.getStream(1).bytesLeftInWriteWindow);
-
- // The stream should no longer be blocked.
- out.flush();
-
- assertEquals(0, connection.bytesLeftInWriteWindow);
- assertEquals(0, connection.getStream(1).bytesLeftInWriteWindow);
-
- // Settings after the initial do not affect the connection window size.
- Settings next = new Settings();
- next.set(Settings.INITIAL_WINDOW_SIZE, PERSIST_VALUE, DEFAULT_INITIAL_WINDOW_SIZE + 2);
- connection.readerRunnable.settings(false, next);
-
- assertEquals(0, connection.bytesLeftInWriteWindow); // connection wasn't affected.
- assertEquals(1, connection.getStream(1).bytesLeftInWriteWindow);
- }
-
@Test public void testTruncatedDataFrame() throws Exception {
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
- peer.sendTruncatedFrame(8 + 100).data(false, 1, data(1024));
+ peer.sendFrame().data(false, 1, data(1024), 1024);
+ peer.truncateLastFrame(8 + 100);
peer.play();
// play it back
@@ -1292,7 +1127,7 @@
}
@Test public void blockedStreamDoesntStarveNewStream() throws Exception {
- int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, SPDY3.maxFrameSize());
+ int framesThatFillWindow = roundUp(DEFAULT_INITIAL_WINDOW_SIZE, peer.maxOutboundDataLength());
// Write the mocking script. This accepts more data frames than necessary!
peer.acceptFrame(); // SYN_STREAM on stream 1
@@ -1331,35 +1166,6 @@
assertEquals(DEFAULT_INITIAL_WINDOW_SIZE - 3, connection.getStream(3).bytesLeftInWriteWindow);
}
- @Test public void maxFrameSizeHonored() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- byte[] buff = new byte[HTTP_20_DRAFT_09.maxFrameSize() + 1];
- Arrays.fill(buff, (byte) '*');
-
- // write the mocking script
- peer.acceptFrame(); // SYN_STREAM
- peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
- peer.acceptFrame(); // DATA 1
- peer.acceptFrame(); // DATA 2
- peer.play();
-
- // play it back
- SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
- SpdyStream stream = connection.newStream(headerEntries("b", "banana"), true, true);
- BufferedSink out = Okio.buffer(stream.getSink());
- out.write(buff);
- out.flush();
- out.close();
-
- MockSpdyPeer.InFrame synStream = peer.takeFrame();
- assertEquals(TYPE_HEADERS, synStream.type);
- MockSpdyPeer.InFrame data = peer.takeFrame();
- assertEquals(HTTP_20_DRAFT_09.maxFrameSize(), data.data.length);
- data = peer.takeFrame();
- assertEquals(1, data.data.length);
- }
-
/** https://github.com/square/okhttp/issues/333 */
@Test public void headerBlockHasTrailingCompressedBytes512() throws Exception {
// This specially-formatted frame has trailing deflated bytes after the name value block.
@@ -1415,8 +1221,10 @@
private void headerBlockHasTrailingCompressedBytes(String frame, int length) throws IOException {
// write the mocking script
peer.acceptFrame(); // SYN_STREAM
- peer.sendFrame(ByteString.decodeBase64(frame).toByteArray());
- peer.sendFrame().data(true, 1, new OkBuffer().writeUtf8("robot"));
+ byte[] trailingCompressedBytes = ByteString.decodeBase64(frame).toByteArray();
+ trailingCompressedBytes[11] = 1; // Set SPDY/3 stream ID to 3.
+ peer.sendFrame(trailingCompressedBytes);
+ peer.sendFrame().data(true, 1, new Buffer().writeUtf8("robot"), 5);
peer.acceptFrame(); // DATA
peer.play();
@@ -1428,131 +1236,6 @@
assertStreamData("robot", stream.getSource());
}
- @Test public void pushPromiseStream() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- // write the mocking script
- peer.acceptFrame(); // SYN_STREAM
- peer.sendFrame().synReply(false, 1, headerEntries("a", "android"));
- final List<Header> expectedRequestHeaders = Arrays.asList(
- new Header(Header.TARGET_METHOD, "GET"),
- new Header(Header.TARGET_SCHEME, "https"),
- new Header(Header.TARGET_AUTHORITY, "squareup.com"),
- new Header(Header.TARGET_PATH, "/cached")
- );
- peer.sendFrame().pushPromise(1, 2, expectedRequestHeaders);
- final List<Header> expectedResponseHeaders = Arrays.asList(
- new Header(Header.RESPONSE_STATUS, "200")
- );
- peer.sendFrame().synReply(true, 2, expectedResponseHeaders);
- peer.sendFrame().data(true, 1, data(0));
- peer.play();
-
- final List events = new ArrayList();
- PushObserver observer = new PushObserver() {
-
- @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
- assertEquals(2, streamId);
- events.add(requestHeaders);
- return false;
- }
-
- @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
- assertEquals(2, streamId);
- assertTrue(last);
- events.add(responseHeaders);
- return false;
- }
-
- @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
- boolean last) throws IOException {
- events.add(new AssertionError("onData"));
- return false;
- }
-
- @Override public void onReset(int streamId, ErrorCode errorCode) {
- events.add(new AssertionError("onReset"));
- }
- };
-
- // play it back
- SpdyConnection connection = connectionBuilder(peer, HTTP_20_DRAFT_09)
- .pushObserver(observer).build();
- SpdyStream client = connection.newStream(headerEntries("b", "banana"), false, true);
- assertEquals(-1, client.getSource().read(new OkBuffer(), 1));
-
- // verify the peer received what was expected
- assertEquals(TYPE_HEADERS, peer.takeFrame().type);
-
- assertEquals(2, events.size());
- assertEquals(expectedRequestHeaders, events.get(0));
- assertEquals(expectedResponseHeaders, events.get(1));
- }
-
- @Test public void doublePushPromise() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- // write the mocking script
- peer.sendFrame().pushPromise(1,2, headerEntries("a", "android"));
- peer.acceptFrame(); // SYN_REPLY
- peer.sendFrame().pushPromise(1, 2, headerEntries("b", "banana"));
- peer.acceptFrame(); // RST_STREAM
- peer.play();
-
- // play it back
- SpdyConnection connection = connectionBuilder(peer, HTTP_20_DRAFT_09).build();
- connection.newStream(headerEntries("b", "banana"), false, true);
-
- // verify the peer received what was expected
- assertEquals(TYPE_HEADERS, peer.takeFrame().type);
- assertEquals(PROTOCOL_ERROR, peer.takeFrame().errorCode);
- }
-
- @Test public void pushPromiseStreamsAutomaticallyCancel() throws Exception {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, false);
-
- // write the mocking script
- peer.sendFrame().pushPromise(1, 2, Arrays.asList(
- new Header(Header.TARGET_METHOD, "GET"),
- new Header(Header.TARGET_SCHEME, "https"),
- new Header(Header.TARGET_AUTHORITY, "squareup.com"),
- new Header(Header.TARGET_PATH, "/cached")
- ));
- peer.sendFrame().synReply(true, 2, Arrays.asList(
- new Header(Header.RESPONSE_STATUS, "200")
- ));
- peer.acceptFrame(); // RST_STREAM
- peer.play();
-
- // play it back
- connectionBuilder(peer, HTTP_20_DRAFT_09)
- .pushObserver(PushObserver.CANCEL).build();
-
- // verify the peer received what was expected
- MockSpdyPeer.InFrame rstStream = peer.takeFrame();
- assertEquals(TYPE_RST_STREAM, rstStream.type);
- assertEquals(2, rstStream.streamId);
- assertEquals(CANCEL, rstStream.errorCode);
- }
-
- private SpdyConnection sendHttp2SettingsAndCheckForAck(boolean client, Settings settings)
- throws IOException, InterruptedException {
- peer.setVariantAndClient(HTTP_20_DRAFT_09, client);
- peer.sendFrame().settings(settings);
- peer.acceptFrame(); // ACK
- peer.play();
-
- // play it back
- SpdyConnection connection = connection(peer, HTTP_20_DRAFT_09);
-
- // verify the peer received the ACK
- MockSpdyPeer.InFrame ackFrame = peer.takeFrame();
- assertEquals(TYPE_SETTINGS, ackFrame.type);
- assertEquals(0, ackFrame.streamId);
- assertTrue(ackFrame.ack);
- return connection;
- }
-
private SpdyConnection connection(MockSpdyPeer peer, Variant variant) throws IOException {
return connectionBuilder(peer, variant).build();
}
@@ -1560,15 +1243,11 @@
private SpdyConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
throws IOException {
return new SpdyConnection.Builder(true, peer.openSocket())
- .pushObserver(IGNORE)
.protocol(variant.getProtocol());
}
private void assertStreamData(String expected, Source source) throws IOException {
- OkBuffer buffer = new OkBuffer();
- while (source.read(buffer, Long.MAX_VALUE) != -1) {
- }
- String actual = buffer.readUtf8(buffer.size());
+ String actual = Okio.buffer(source).readUtf8();
assertEquals(expected, actual);
}
@@ -1599,24 +1278,4 @@
static int roundUp(int num, int divisor) {
return (num + divisor - 1) / divisor;
}
-
- static final PushObserver IGNORE = new PushObserver() {
-
- @Override public boolean onRequest(int streamId, List<Header> requestHeaders) {
- return false;
- }
-
- @Override public boolean onHeaders(int streamId, List<Header> responseHeaders, boolean last) {
- return false;
- }
-
- @Override public boolean onData(int streamId, BufferedSource source, int byteCount,
- boolean last) throws IOException {
- source.skip(byteCount);
- return false;
- }
-
- @Override public void onReset(int streamId, ErrorCode errorCode) {
- }
- };
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
index 1904b90..c902773 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/spdy/Spdy3Test.java
@@ -17,8 +17,8 @@
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
+import okio.Buffer;
import okio.ByteString;
-import okio.OkBuffer;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -29,7 +29,7 @@
@Test public void tooLargeDataFrame() throws IOException {
try {
- sendDataFrame(new OkBuffer().write(new byte[0x1000000]));
+ sendDataFrame(new Buffer().write(new byte[0x1000000]));
fail();
} catch (IllegalArgumentException e) {
assertEquals("FRAME_TOO_LARGE max size is 16Mib: " + 0x1000000L, e.getMessage());
@@ -53,7 +53,7 @@
}
@Test public void goAwayRoundTrip() throws IOException {
- OkBuffer frame = new OkBuffer();
+ Buffer frame = new Buffer();
final ErrorCode expectedError = ErrorCode.PROTOCOL_ERROR;
@@ -83,18 +83,18 @@
});
}
- private void sendDataFrame(OkBuffer source) throws IOException {
- Spdy3.Writer writer = new Spdy3.Writer(new OkBuffer(), true);
+ private void sendDataFrame(Buffer source) throws IOException {
+ Spdy3.Writer writer = new Spdy3.Writer(new Buffer(), true);
writer.sendDataFrame(expectedStreamId, 0, source, (int) source.size());
}
private void windowUpdate(long increment) throws IOException {
- new Spdy3.Writer(new OkBuffer(), true).windowUpdate(expectedStreamId, increment);
+ new Spdy3.Writer(new Buffer(), true).windowUpdate(expectedStreamId, increment);
}
- private OkBuffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
+ private Buffer sendGoAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData)
throws IOException {
- OkBuffer out = new OkBuffer();
+ Buffer out = new Buffer();
new Spdy3.Writer(out, true).goAway(lastGoodStreamId, errorCode, debugData);
return out;
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
new file mode 100644
index 0000000..7e7f05a
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Random;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ByteString;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.TEXT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class RealWebSocketTest {
+ // NOTE: Types are named 'client' and 'server' for cognitive simplicity. This differentiation has
+ // 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 RealWebSocket client;
+ private boolean clientConnectionClosed;
+ private final Buffer client2Server = new Buffer();
+ private final WebSocketRecorder clientListener = new WebSocketRecorder();
+
+ private RealWebSocket server;
+ private final Buffer server2client = new Buffer();
+ private final WebSocketRecorder serverListener = new WebSocketRecorder();
+
+ @Before public void setUp() {
+ Random random = new Random(0);
+
+ client = new RealWebSocket(true, server2client, client2Server, random, clientListener,
+ "http://example.com/websocket") {
+ @Override protected void closeConnection() throws IOException {
+ clientConnectionClosed = true;
+ }
+ };
+ server = new RealWebSocket(false, client2Server, server2client, random, serverListener,
+ "http://example.com/websocket") {
+ @Override protected void closeConnection() throws IOException {
+ }
+ };
+ }
+
+ @After public void tearDown() {
+ clientListener.assertExhausted();
+ serverListener.assertExhausted();
+ }
+
+ @Test public void textMessage() throws IOException {
+ client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ server.readMessage();
+ serverListener.assertTextMessage("Hello!");
+ }
+
+ @Test public void binaryMessage() throws IOException {
+ client.sendMessage(BINARY, new Buffer().writeUtf8("Hello!"));
+ server.readMessage();
+ serverListener.assertBinaryMessage(new byte[] { 'H', 'e', 'l', 'l', 'o', '!' });
+ }
+
+ @Test public void streamingMessage() throws IOException {
+ BufferedSink sink = client.newMessageSink(TEXT);
+ sink.writeUtf8("Hel").flush();
+ sink.writeUtf8("lo!").flush();
+ sink.close();
+ 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();
+ server.readMessage();
+ serverListener.assertTextMessage("Hello!");
+ Thread.sleep(1000); // Wait for pong to be written.
+ client.readMessage();
+ clientListener.assertPong(new Buffer().writeUtf8("Pong?"));
+ }
+
+ @Test public void pingWritesPong() throws IOException, InterruptedException {
+ client.sendPing(new Buffer().writeUtf8("Hello!"));
+ server.readMessage(); // Read the ping, enqueue the pong.
+ Thread.sleep(1000); // Wait for pong to be written.
+ client.readMessage();
+ clientListener.assertPong(new Buffer().writeUtf8("Hello!"));
+ }
+
+ @Test public void unsolicitedPong() throws IOException {
+ client.sendPong(new Buffer().writeUtf8("Hello!"));
+ server.readMessage();
+ serverListener.assertPong(new Buffer().writeUtf8("Hello!"));
+ }
+
+ @Test public void close() throws IOException {
+ client.close(1000, "Hello!");
+ server.readMessage(); // This will trigger a close response.
+ serverListener.assertClose(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+ }
+
+ @Test public void clientCloseThenMethodsThrow() throws IOException {
+ client.close(1000, "Hello!");
+
+ try {
+ client.sendPing(new Buffer().writeUtf8("Pong?"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ try {
+ client.close(1000, "Hello!");
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ try {
+ client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ try {
+ client.newMessageSink(TEXT);
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ }
+
+ @Test public void serverCloseThenWritingThrows() throws IOException {
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
+ try {
+ client.sendPing(new Buffer().writeUtf8("Pong?"));
+ fail();
+ } catch (IOException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ try {
+ client.sendMessage(TEXT, new Buffer().writeUtf8("Hi!"));
+ fail();
+ } catch (IOException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ try {
+ client.close(1000, "Bye!");
+ fail();
+ } catch (IOException e) {
+ assertEquals("Closed", e.getMessage());
+ }
+ }
+
+ @Test public void serverCloseWhileWritingThrows() throws IOException {
+ // Start writing data.
+ BufferedSink sink = client.newMessageSink(TEXT);
+ sink.writeUtf8("Hel").flush();
+
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
+ 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());
+ }
+ }
+
+ @Test public void clientCloseClosesConnection() throws IOException {
+ client.close(1000, "Hello!");
+ assertFalse(clientConnectionClosed);
+ server.readMessage(); // Read client close, send server close.
+ serverListener.assertClose(1000, "Hello!");
+
+ client.readMessage();
+ assertTrue(clientConnectionClosed);
+ clientListener.assertClose(1000, "Hello!");
+ }
+
+ @Test public void serverCloseClosesConnection() throws IOException {
+ server.close(1000, "Hello!");
+
+ client.readMessage(); // Read server close, send client close, close connection.
+ clientListener.assertClose(1000, "Hello!");
+ assertTrue(clientConnectionClosed);
+
+ server.readMessage();
+ serverListener.assertClose(1000, "Hello!");
+ }
+
+ @Test public void clientAndServerCloseClosesConnection() throws IOException {
+ // Send close from both sides at the same time.
+ server.close(1000, "Hello!");
+ client.close(1000, "Hi!");
+ assertFalse(clientConnectionClosed);
+
+ client.readMessage(); // Read close, should NOT send close.
+ assertTrue(clientConnectionClosed);
+ clientListener.assertClose(1000, "Hello!");
+
+ server.readMessage();
+ serverListener.assertClose(1000, "Hi!");
+
+ serverListener.assertExhausted(); // Client should not have sent second close.
+ clientListener.assertExhausted(); // Server should not have sent second close.
+ }
+
+ @Test public void serverCloseBreaksReadMessageLoop() throws IOException {
+ server.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ server.close(1000, "Bye!");
+ assertTrue(client.readMessage());
+ clientListener.assertTextMessage("Hello!");
+ assertFalse(client.readMessage());
+ clientListener.assertClose(1000, "Bye!");
+ }
+
+ @Test public void protocolErrorBeforeCloseSendsClose() {
+ server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+
+ client.readMessage(); // Detects error, send close.
+ clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+ assertTrue(clientConnectionClosed);
+
+ server.readMessage();
+ serverListener.assertClose(1002, "");
+ }
+
+ @Test public void protocolErrorAfterCloseDoesNotSendClose() throws IOException {
+ client.close(1000, "Hello!");
+ server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+
+ client.readMessage();
+ clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+ assertTrue(clientConnectionClosed);
+
+ server.readMessage();
+ serverListener.assertClose(1000, "Hello!");
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketCallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketCallTest.java
new file mode 100644
index 0000000..705b035
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketCallTest.java
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.RecordedResponse;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Random;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+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.internal.ws.WebSocket.PayloadType.TEXT;
+
+public final class WebSocketCallTest {
+ @Rule public final MockWebServerRule server = new MockWebServerRule();
+
+ private final WebSocketRecorder listener = new WebSocketRecorder();
+ private final OkHttpClient client = new OkHttpClient();
+ private final Random random = new Random(0);
+
+ @After public void tearDown() {
+ listener.assertExhausted();
+ }
+
+ @Test public void clientPingPong() throws IOException {
+ WebSocketListener serverListener = new EmptyWebSocketListener();
+ server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
+
+ WebSocket webSocket = awaitCall().webSocket;
+ webSocket.sendPing(new Buffer().writeUtf8("Hello, WebSockets!"));
+ listener.assertPong(new Buffer().writeUtf8("Hello, WebSockets!"));
+ }
+
+ @Test public void clientMessage() throws IOException {
+ WebSocketRecorder serverListener = new WebSocketRecorder();
+ server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
+
+ WebSocket webSocket = awaitCall().webSocket;
+ webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+ serverListener.assertTextMessage("Hello, WebSockets!");
+ }
+
+ @Test public void serverMessage() throws IOException {
+ WebSocketListener serverListener = new EmptyWebSocketListener() {
+ @Override public void onOpen(WebSocket webSocket, Request request, Response response)
+ throws IOException {
+ webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+ }
+ };
+ server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
+
+ awaitCall();
+ listener.assertTextMessage("Hello, WebSockets!");
+ }
+
+ @Test public void clientStreamingMessage() throws IOException {
+ WebSocketRecorder serverListener = new WebSocketRecorder();
+ server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
+
+ WebSocket webSocket = awaitCall().webSocket;
+ 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(WebSocket webSocket, Request request, Response response)
+ throws IOException {
+ BufferedSink sink = webSocket.newMessageSink(TEXT);
+ sink.writeUtf8("Hello, ").flush();
+ sink.writeUtf8("WebSockets!").flush();
+ sink.close();
+ }
+ };
+ server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
+
+ awaitCall();
+ listener.assertTextMessage("Hello, WebSockets!");
+ }
+
+ @Test public void okButNotOk() {
+ server.enqueue(new MockResponse());
+ awaitCall();
+ listener.assertFailure(ProtocolException.class, "Expected HTTP 101 response but was '200 OK'");
+ }
+
+ @Test public void notFound() {
+ server.enqueue(new MockResponse().setStatus("HTTP/1.1 404 Not Found"));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected HTTP 101 response but was '404 Not Found'");
+ }
+
+ @Test public void missingConnectionHeader() {
+ server.enqueue(new MockResponse()
+ .setResponseCode(101)
+ .setHeader("Upgrade", "websocket")
+ .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected 'Connection' header value 'Upgrade' but was 'null'");
+ }
+
+ @Test public void wrongConnectionHeader() {
+ server.enqueue(new MockResponse().setResponseCode(101)
+ .setHeader("Upgrade", "websocket")
+ .setHeader("Connection", "Downgrade")
+ .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected 'Connection' header value 'Upgrade' but was 'Downgrade'");
+ }
+
+ @Test public void missingUpgradeHeader() {
+ server.enqueue(new MockResponse()
+ .setResponseCode(101)
+ .setHeader("Connection", "Upgrade")
+ .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected 'Upgrade' header value 'websocket' but was 'null'");
+ }
+
+ @Test public void wrongUpgradeHeader() {
+ server.enqueue(new MockResponse()
+ .setResponseCode(101)
+ .setHeader("Connection", "Upgrade")
+ .setHeader("Upgrade", "Pepsi")
+ .setHeader("Sec-WebSocket-Accept", "ujmZX4KXZqjwy6vi1aQFH5p4Ygk="));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected 'Upgrade' header value 'websocket' but was 'Pepsi'");
+ }
+
+ @Test public void missingMagicHeader() {
+ server.enqueue(new MockResponse()
+ .setResponseCode(101)
+ .setHeader("Connection", "Upgrade")
+ .setHeader("Upgrade", "websocket"));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'null'");
+ }
+
+ @Test public void wrongMagicHeader() {
+ server.enqueue(new MockResponse()
+ .setResponseCode(101)
+ .setHeader("Connection", "Upgrade")
+ .setHeader("Upgrade", "websocket")
+ .setHeader("Sec-WebSocket-Accept", "magic"));
+ awaitCall();
+ listener.assertFailure(ProtocolException.class,
+ "Expected 'Sec-WebSocket-Accept' header value 'ujmZX4KXZqjwy6vi1aQFH5p4Ygk=' but was 'magic'");
+ }
+
+ private RecordedResponse awaitCall() {
+ Request request = new Request.Builder().get().url(server.getUrl("/")).build();
+ WebSocketCall call = new WebSocketCall(client, request, random);
+
+ final AtomicReference<Response> responseRef = new AtomicReference<>();
+ final AtomicReference<WebSocket> webSocketRef = new AtomicReference<>();
+ final AtomicReference<IOException> failureRef = new AtomicReference<>();
+ final CountDownLatch latch = new CountDownLatch(1);
+ call.enqueue(new WebSocketListener() {
+ @Override public void onOpen(WebSocket webSocket, Request request, Response response)
+ throws IOException {
+ webSocketRef.set(webSocket);
+ responseRef.set(response);
+ latch.countDown();
+ }
+
+ @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
+ throws IOException {
+ listener.onMessage(payload, type);
+ }
+
+ @Override public void onPong(Buffer payload) {
+ listener.onPong(payload);
+ }
+
+ @Override public void onClose(int code, String reason) {
+ listener.onClose(code, reason);
+ }
+
+ @Override public void onFailure(IOException e) {
+ listener.onFailure(e);
+ failureRef.set(e);
+ latch.countDown();
+ }
+ });
+
+ try {
+ latch.await(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ if (latch.getCount() > 0) {
+ throw new AssertionError("Timed out.");
+ }
+
+ return new RecordedResponse(request, responseRef.get(), webSocketRef.get(), null,
+ failureRef.get());
+ }
+
+ private static class EmptyWebSocketListener implements WebSocketListener {
+ @Override public void onOpen(WebSocket webSocket, Request request, Response response)
+ throws IOException {
+ }
+
+ @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
+ throws IOException {
+ }
+
+ @Override public void onPong(Buffer payload) {
+ }
+
+ @Override public void onClose(int code, String reason) {
+ }
+
+ @Override public void onFailure(IOException e) {
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
new file mode 100644
index 0000000..2f9dda8
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicReference;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.ByteString;
+import org.junit.After;
+import org.junit.Test;
+
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType;
+import static com.squareup.okhttp.internal.ws.WebSocketRecorder.MessageDelegate;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+public class WebSocketReaderTest {
+ private final Buffer data = new Buffer();
+ private final WebSocketRecorder callback = new WebSocketRecorder();
+ private final Random random = new Random(0);
+
+ // Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test.
+ private final WebSocketReader serverReader = new WebSocketReader(false, data, callback);
+ private final WebSocketReader clientReader = new WebSocketReader(true, data, callback);
+
+ @After public void tearDown() {
+ callback.assertExhausted();
+ }
+
+ @Test public void controlFramesMustBeFinal() throws IOException {
+ data.write(ByteString.decodeHex("0a00")); // Empty ping.
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Control frames must be final.", e.getMessage());
+ }
+ }
+
+ @Test public void reservedFlagsAreUnsupported() throws IOException {
+ data.write(ByteString.decodeHex("9a00")); // Empty ping, flag 1 set.
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Reserved flags are unsupported.", e.getMessage());
+ }
+ data.clear();
+ data.write(ByteString.decodeHex("aa00")); // Empty ping, flag 2 set.
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Reserved flags are unsupported.", e.getMessage());
+ }
+ data.clear();
+ data.write(ByteString.decodeHex("ca00")); // Empty ping, flag 3 set.
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Reserved flags are unsupported.", e.getMessage());
+ }
+ }
+
+ @Test public void clientSentFramesMustBeMasked() throws IOException {
+ data.write(ByteString.decodeHex("8100"));
+ try {
+ serverReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Client-sent frames must be masked. Server sent must not.", e.getMessage());
+ }
+ }
+
+ @Test public void serverSentFramesMustNotBeMasked() throws IOException {
+ data.write(ByteString.decodeHex("8180"));
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Client-sent frames must be masked. Server sent must not.", e.getMessage());
+ }
+ }
+
+ @Test public void controlFramePayloadMax() throws IOException {
+ data.write(ByteString.decodeHex("8a7e007e"));
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Control frame must be less than 125B.", e.getMessage());
+ }
+ }
+
+ @Test public void clientSimpleHello() throws IOException {
+ data.write(ByteString.decodeHex("810548656c6c6f")); // Hello
+ clientReader.processNextFrame();
+ callback.assertTextMessage("Hello");
+ }
+
+ @Test public void serverSimpleHello() throws IOException {
+ data.write(ByteString.decodeHex("818537fa213d7f9f4d5158")); // Hello
+ serverReader.processNextFrame();
+ callback.assertTextMessage("Hello");
+ }
+
+ @Test public void serverHelloTwoChunks() throws IOException {
+ data.write(ByteString.decodeHex("818537fa213d7f9f4d")); // Hel
+
+ 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"
+ data.write(ByteString.decodeHex("5158")); // lo
+ payload.readFully(sink, 2); // Read "lo"
+ payload.close();
+ }
+ });
+ serverReader.processNextFrame();
+
+ assertEquals("Hello", sink.readUtf8());
+ }
+
+ @Test public void clientTwoFrameHello() throws IOException {
+ data.write(ByteString.decodeHex("010348656c")); // Hel
+ data.write(ByteString.decodeHex("80026c6f")); // lo
+ clientReader.processNextFrame();
+ callback.assertTextMessage("Hello");
+ }
+
+ @Test public void clientTwoFrameHelloWithPongs() throws IOException {
+ data.write(ByteString.decodeHex("010348656c")); // Hel
+ data.write(ByteString.decodeHex("8a00")); // Pong
+ data.write(ByteString.decodeHex("8a00")); // Pong
+ data.write(ByteString.decodeHex("8a00")); // Pong
+ data.write(ByteString.decodeHex("8a00")); // Pong
+ data.write(ByteString.decodeHex("80026c6f")); // lo
+ clientReader.processNextFrame();
+ callback.assertPong(null);
+ callback.assertPong(null);
+ callback.assertPong(null);
+ callback.assertPong(null);
+ callback.assertTextMessage("Hello");
+ }
+
+ @Test public void clientIncompleteMessageBodyThrows() throws IOException {
+ data.write(ByteString.decodeHex("810548656c")); // Length = 5, "Hel"
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (EOFException ignored) {
+ }
+ }
+
+ @Test public void clientIncompleteControlFrameBodyThrows() throws IOException {
+ data.write(ByteString.decodeHex("8a0548656c")); // Length = 5, "Hel"
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (EOFException ignored) {
+ }
+ }
+
+ @Test public void serverIncompleteMessageBodyThrows() throws IOException {
+ data.write(ByteString.decodeHex("818537fa213d7f9f4d")); // Length = 5, "Hel"
+ try {
+ serverReader.processNextFrame();
+ fail();
+ } catch (EOFException ignored) {
+ }
+ }
+
+ @Test public void serverIncompleteControlFrameBodyThrows() throws IOException {
+ data.write(ByteString.decodeHex("8a8537fa213d7f9f4d")); // Length = 5, "Hel"
+ try {
+ serverReader.processNextFrame();
+ fail();
+ } catch (EOFException ignored) {
+ }
+ }
+
+ @Test public void clientSimpleBinary() throws IOException {
+ byte[] bytes = binaryData(256);
+ data.write(ByteString.decodeHex("827E0100")).write(bytes);
+ clientReader.processNextFrame();
+ callback.assertBinaryMessage(bytes);
+ }
+
+ @Test public void clientTwoFrameBinary() throws IOException {
+ byte[] bytes = binaryData(200);
+ data.write(ByteString.decodeHex("0264")).write(bytes, 0, 100);
+ data.write(ByteString.decodeHex("8064")).write(bytes, 100, 100);
+ clientReader.processNextFrame();
+ callback.assertBinaryMessage(bytes);
+ }
+
+ @Test public void twoFrameNotContinuation() throws IOException {
+ byte[] bytes = binaryData(200);
+ data.write(ByteString.decodeHex("0264")).write(bytes, 0, 100);
+ data.write(ByteString.decodeHex("8264")).write(bytes, 100, 100);
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Expected continuation opcode. Got: 2", e.getMessage());
+ }
+ }
+
+ @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());
+ }
+ });
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("Listener failed to call close on message payload.", e.getMessage());
+ }
+ }
+
+ @Test public void closeExhaustsMessage() throws IOException {
+ data.write(ByteString.decodeHex("810548656c6c6f")); // Hello
+ data.write(ByteString.decodeHex("810448657921")); // Hey!
+
+ 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();
+ }
+ });
+
+ clientReader.processNextFrame();
+ assertEquals("Hel", sink.readUtf8());
+
+ clientReader.processNextFrame();
+ callback.assertTextMessage("Hey!");
+ }
+
+ @Test public void closeExhaustsMessageOverControlFrames() throws IOException {
+ data.write(ByteString.decodeHex("010348656c")); // Hel
+ data.write(ByteString.decodeHex("8a00")); // Pong
+ data.write(ByteString.decodeHex("8a00")); // Pong
+ data.write(ByteString.decodeHex("80026c6f")); // lo
+ data.write(ByteString.decodeHex("810448657921")); // Hey!
+
+ 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();
+ }
+ });
+
+ clientReader.processNextFrame();
+ assertEquals("He", sink.readUtf8());
+ callback.assertPong(null);
+ callback.assertPong(null);
+
+ clientReader.processNextFrame();
+ callback.assertTextMessage("Hey!");
+ }
+
+ @Test public void closedMessageSourceThrows() throws IOException {
+ data.write(ByteString.decodeHex("810548656c6c6f")); // Hello
+
+ final AtomicReference<Exception> exception = new AtomicReference<>();
+ callback.setNextMessageDelegate(new MessageDelegate() {
+ @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
+ payload.close();
+ try {
+ payload.readAll(new Buffer());
+ fail();
+ } catch (IllegalStateException e) {
+ exception.set(e);
+ }
+ }
+ });
+ clientReader.processNextFrame();
+
+ assertNotNull(exception.get());
+ }
+
+ @Test public void emptyPingCallsCallback() throws IOException {
+ data.write(ByteString.decodeHex("8900")); // Empty ping
+ clientReader.processNextFrame();
+ callback.assertPing(null);
+ }
+
+ @Test public void pingCallsCallback() throws IOException {
+ data.write(ByteString.decodeHex("890548656c6c6f")); // Ping with "Hello"
+ clientReader.processNextFrame();
+ callback.assertPing(new Buffer().writeUtf8("Hello"));
+ }
+
+ @Test public void emptyCloseCallsCallback() throws IOException {
+ data.write(ByteString.decodeHex("8800")); // Empty close
+ clientReader.processNextFrame();
+ callback.assertClose(0, "");
+ }
+
+ @Test public void closeCallsCallback() throws IOException {
+ data.write(ByteString.decodeHex("880703e848656c6c6f")); // Close with code and reason
+ clientReader.processNextFrame();
+ callback.assertClose(1000, "Hello");
+ }
+
+ private byte[] binaryData(int length) {
+ byte[] junk = new byte[length];
+ random.nextBytes(junk);
+ return junk;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketRecorder.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketRecorder.java
new file mode 100644
index 0000000..05b8480
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketRecorder.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+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.internal.ws.WebSocket.PayloadType.BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.TEXT;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+final class WebSocketRecorder implements WebSocketReader.FrameCallback, WebSocketListener {
+ public interface MessageDelegate {
+ void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException;
+ }
+
+ private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
+ private MessageDelegate delegate;
+
+ /** Sets a delegate for the next call to {@link #onMessage}. Cleared after invoked. */
+ public void setNextMessageDelegate(MessageDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override public void onOpen(WebSocket webSocket, Request request, Response response) {
+ }
+
+ @Override public void onMessage(BufferedSource source, WebSocket.PayloadType type)
+ throws IOException {
+ if (delegate != null) {
+ delegate.onMessage(source, type);
+ delegate = null;
+ } else {
+ Message message = new Message(type);
+ source.readAll(message.buffer);
+ source.close();
+ events.add(message);
+ }
+ }
+
+ @Override public void onPing(Buffer buffer) {
+ events.add(new Ping(buffer));
+ }
+
+ @Override public void onPong(Buffer buffer) {
+ events.add(new Pong(buffer));
+ }
+
+ @Override public void onClose(int code, String reason) {
+ events.add(new Close(code, reason));
+ }
+
+ @Override public void onFailure(IOException e) {
+ events.add(e);
+ }
+
+ private Object nextEvent() {
+ try {
+ return events.poll(10, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ public void assertTextMessage(String payload) {
+ Message message = new Message(TEXT);
+ message.buffer.writeUtf8(payload);
+ assertEquals(message, nextEvent());
+ }
+
+ public void assertBinaryMessage(byte[] payload) {
+ Message message = new Message(BINARY);
+ message.buffer.write(payload);
+ assertEquals(message, nextEvent());
+ }
+
+ public void assertPing(Buffer payload) {
+ assertEquals(new Ping(payload), nextEvent());
+ }
+
+ public void assertPong(Buffer payload) {
+ assertEquals(new Pong(payload), nextEvent());
+ }
+
+ public void assertClose(int code, String reason) {
+ assertEquals(new Close(code, reason), nextEvent());
+ }
+
+ public void assertFailure(Class<? extends IOException> cls, String message) {
+ Object event = nextEvent();
+ String errorMessage =
+ "Expected [" + cls.getName() + ": " + message + "] but was [" + event + "].";
+ assertNotNull(errorMessage, event);
+ assertEquals(errorMessage, cls, event.getClass());
+ assertEquals(errorMessage, cls.cast(event).getMessage(), message);
+ }
+
+ public void assertExhausted() {
+ assertTrue("Remaining events: " + events, events.isEmpty());
+ }
+
+ private static class Message {
+ public final WebSocket.PayloadType type;
+ public final Buffer buffer = new Buffer();
+
+ private Message(WebSocket.PayloadType type) {
+ this.type = type;
+ }
+
+ @Override public String toString() {
+ return "Message[" + type + " " + buffer + "]";
+ }
+
+ @Override public int hashCode() {
+ return type.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 false;
+ }
+ }
+
+ private static class Ping {
+ public final Buffer buffer;
+
+ private Ping(Buffer buffer) {
+ this.buffer = buffer;
+ }
+
+ @Override public String toString() {
+ return "Ping[" + buffer + "]";
+ }
+
+ @Override public int hashCode() {
+ return buffer.hashCode();
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (obj instanceof Ping) {
+ Ping other = (Ping) obj;
+ return buffer == null ? other.buffer == null : buffer.equals(other.buffer);
+ }
+ return false;
+ }
+ }
+
+ private static class Pong {
+ public final Buffer buffer;
+
+ private Pong(Buffer buffer) {
+ this.buffer = buffer;
+ }
+
+ @Override public String toString() {
+ return "Pong[" + buffer + "]";
+ }
+
+ @Override public int hashCode() {
+ return buffer.hashCode();
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (obj instanceof Pong) {
+ Pong other = (Pong) obj;
+ return buffer == null ? other.buffer == null : buffer.equals(other.buffer);
+ }
+ return false;
+ }
+ }
+
+ private static class Close {
+ public final int code;
+ public final String reason;
+
+ private Close(int code, String reason) {
+ this.code = code;
+ this.reason = reason;
+ }
+
+ @Override public String toString() {
+ return "Close[" + code + " " + reason + "]";
+ }
+
+ @Override public int hashCode() {
+ return code * 37 + reason.hashCode();
+ }
+
+ @Override public boolean equals(Object obj) {
+ if (obj instanceof Close) {
+ Close other = (Close) obj;
+ return code == other.code && reason.equals(other.reason);
+ }
+ return false;
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
new file mode 100644
index 0000000..141d9ca
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
@@ -0,0 +1,335 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+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 org.junit.Test;
+
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class WebSocketWriterTest {
+ private final Buffer data = new Buffer();
+ private final Random random = new Random(0);
+
+ // 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);
+
+ sink.writeUtf8("Hel").flush();
+ assertData("010348656c");
+
+ sink.writeUtf8("lo").flush();
+ assertData("00026c6f");
+
+ sink.close();
+ assertData("8000");
+ }
+
+ @Test public void serverStreamCloseFlushes() throws IOException {
+ BufferedSink sink = serverWriter.newMessageSink(TEXT);
+
+ sink.writeUtf8("Hel").flush();
+ assertData("010348656c");
+
+ sink.writeUtf8("lo").close();
+ assertData("00026c6f");
+ assertData("8000");
+ }
+
+ @Test public void clientStreamSimpleHello() throws IOException {
+ BufferedSink sink = clientWriter.newMessageSink(TEXT);
+
+ sink.writeUtf8("Hel").flush();
+ assertData("018360b420bb28d14c");
+
+ sink.writeUtf8("lo").flush();
+ assertData("00823851d9d4543e");
+
+ sink.close();
+ 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 serverSendBinaryShort() throws IOException {
+ byte[] payload = binaryData(1000);
+ serverWriter.sendMessage(BINARY, new Buffer().write(payload));
+ assertData("827e03e8");
+ 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();
+ assertData("0232");
+ assertData(Arrays.copyOfRange(payload, 0, 50));
+
+ sink.write(payload, 50, 50).flush();
+ assertData("0032");
+ assertData(Arrays.copyOfRange(payload, 50, 100));
+
+ sink.close();
+ assertData("8000");
+ }
+
+ @Test public void clientStreamBinary() 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);
+
+ sink.write(payload, 0, 50).flush();
+ byte[] part1 = Arrays.copyOfRange(payload, 0, 50);
+ toggleMask(part1, 50, maskKey1, 0);
+ assertData("02b2");
+ assertData(maskKey1);
+ assertData(part1);
+
+ sink.write(payload, 50, 50).flush();
+ byte[] part2 = Arrays.copyOfRange(payload, 50, 100);
+ toggleMask(part2, 50, maskKey2, 0);
+ assertData("00b2");
+ assertData(maskKey2);
+ assertData(part2);
+
+ sink.close();
+ assertData("8080");
+ assertData(maskKey3);
+ }
+
+ @Test public void serverEmptyClose() throws IOException {
+ serverWriter.writeClose(0, null);
+ assertData("8800");
+ }
+
+ @Test public void serverCloseWithCode() throws IOException {
+ serverWriter.writeClose(1005, null);
+ assertData("880203ed");
+ }
+
+ @Test public void serverCloseWithCodeAndReason() throws IOException {
+ serverWriter.writeClose(1005, "Hello");
+ assertData("880703ed48656c6c6f");
+ }
+
+ @Test public void clientEmptyClose() throws IOException {
+ clientWriter.writeClose(0, null);
+ assertData("888060b420bb");
+ }
+
+ @Test public void clientCloseWithCode() throws IOException {
+ clientWriter.writeClose(1005, null);
+ assertData("888260b420bb6359");
+ }
+
+ @Test public void clientCloseWithCodeAndReason() throws IOException {
+ clientWriter.writeClose(1005, "Hello");
+ assertData("888760b420bb635968de0cd84f");
+ }
+
+ @Test public void closeWithOnlyReasonThrows() throws IOException {
+ try {
+ clientWriter.writeClose(0, "Hello");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Code required to include reason.", e.getMessage());
+ }
+ }
+
+ @Test public void closeCodeOutOfRangeThrows() throws IOException {
+ try {
+ clientWriter.writeClose(98724976, "Hello");
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Code must be in range [1000,5000).", e.getMessage());
+ }
+ }
+
+ @Test public void serverEmptyPing() throws IOException {
+ serverWriter.writePing(null);
+ assertData("8900");
+ }
+
+ @Test public void clientEmptyPing() throws IOException {
+ clientWriter.writePing(null);
+ assertData("898060b420bb");
+ }
+
+ @Test public void serverPingWithPayload() throws IOException {
+ serverWriter.writePing(new Buffer().writeUtf8("Hello"));
+ assertData("890548656c6c6f");
+ }
+
+ @Test public void clientPingWithPayload() throws IOException {
+ clientWriter.writePing(new Buffer().writeUtf8("Hello"));
+ assertData("898560b420bb28d14cd70f");
+ }
+
+ @Test public void serverEmptyPong() throws IOException {
+ serverWriter.writePong(null);
+ assertData("8a00");
+ }
+
+ @Test public void clientEmptyPong() throws IOException {
+ clientWriter.writePong(null);
+ assertData("8a8060b420bb");
+ }
+
+ @Test public void serverPongWithPayload() throws IOException {
+ serverWriter.writePong(new Buffer().writeUtf8("Hello"));
+ assertData("8a0548656c6c6f");
+ }
+
+ @Test public void clientPongWithPayload() throws IOException {
+ clientWriter.writePong(new Buffer().writeUtf8("Hello"));
+ assertData("8a8560b420bb28d14cd70f");
+ }
+
+ @Test public void pingTooLongThrows() throws IOException {
+ try {
+ serverWriter.writePing(new Buffer().write(binaryData(1000)));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Payload size must be less than or equal to 125", e.getMessage());
+ }
+ }
+
+ @Test public void pongTooLongThrows() throws IOException {
+ try {
+ serverWriter.writePong(new Buffer().write(binaryData(1000)));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Payload size must be less than or equal to 125", e.getMessage());
+ }
+ }
+
+ @Test public void closeTooLongThrows() throws IOException {
+ try {
+ String longString = ByteString.of(binaryData(75)).hex();
+ serverWriter.writeClose(1000, longString);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Payload size must be less than or equal to 125", e.getMessage());
+ }
+ }
+
+ @Test public void twoWritersThrows() {
+ clientWriter.newMessageSink(TEXT);
+ try {
+ clientWriter.newMessageSink(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());
+ assertEquals(expected, actual);
+ }
+
+ private void assertData(byte[] data) throws IOException {
+ int byteCount = 16;
+ for (int i = 0; i < data.length; i += byteCount) {
+ int count = Math.min(byteCount, data.length - i);
+ Buffer expectedChunk = new Buffer();
+ expectedChunk.write(data, i, count);
+ assertEquals("At " + i, expectedChunk.readByteString(), this.data.readByteString(count));
+ }
+ }
+
+ private static byte[] binaryData(int length) {
+ byte[] junk = new byte[length];
+ new Random(0).nextBytes(junk);
+ return junk;
+ }
+}
diff --git a/okhttp-urlconnection/pom.xml b/okhttp-urlconnection/pom.xml
new file mode 100644
index 0000000..790f596
--- /dev/null
+++ b/okhttp-urlconnection/pom.xml
@@ -0,0 +1,49 @@
+<?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.3.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>okhttp-urlconnection</artifactId>
+ <name>OkHttp URLConnection</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>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <excludePackageNames>com.squareup.okhttp.internal.*</excludePackageNames>
+ <links>
+ <link>http://square.github.io/okhttp/javadoc/</link>
+ </links>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java
new file mode 100644
index 0000000..4b34559
--- /dev/null
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2014 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.huc.HttpURLConnectionImpl;
+import com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl;
+
+import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLStreamHandler;
+import java.net.URLStreamHandlerFactory;
+
+public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable {
+ private final OkHttpClient client;
+
+ public OkUrlFactory(OkHttpClient client) {
+ this.client = client;
+ }
+
+ public OkHttpClient client() {
+ return client;
+ }
+
+ /**
+ * Returns a copy of this stream handler factory that includes a shallow copy
+ * of the internal {@linkplain OkHttpClient HTTP client}.
+ */
+ @Override public OkUrlFactory clone() {
+ return new OkUrlFactory(client.clone());
+ }
+
+ public HttpURLConnection open(URL url) {
+ return open(url, client.getProxy());
+ }
+
+ HttpURLConnection open(URL url, Proxy proxy) {
+ String protocol = url.getProtocol();
+ OkHttpClient copy = client.copyWithDefaults();
+ copy.setProxy(proxy);
+
+ if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
+ if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
+ throw new IllegalArgumentException("Unexpected protocol: " + protocol);
+ }
+
+ /**
+ * Creates a URLStreamHandler as a {@link java.net.URL#setURLStreamHandlerFactory}.
+ *
+ * <p>This code configures OkHttp to handle all HTTP and HTTPS connections
+ * created with {@link java.net.URL#openConnection()}: <pre> {@code
+ *
+ * OkHttpClient okHttpClient = new OkHttpClient();
+ * URL.setURLStreamHandlerFactory(new OkUrlFactory(okHttpClient));
+ * }</pre>
+ */
+ @Override public URLStreamHandler createURLStreamHandler(final String protocol) {
+ if (!protocol.equals("http") && !protocol.equals("https")) return null;
+
+ return new URLStreamHandler() {
+ @Override protected URLConnection openConnection(URL url) {
+ return open(url);
+ }
+
+ @Override protected URLConnection openConnection(URL url, Proxy proxy) {
+ return open(url, proxy);
+ }
+
+ @Override protected int getDefaultPort() {
+ if (protocol.equals("http")) return 80;
+ if (protocol.equals("https")) return 443;
+ throw new AssertionError();
+ }
+ };
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/DelegatingHttpsURLConnection.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/DelegatingHttpsURLConnection.java
similarity index 99%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/DelegatingHttpsURLConnection.java
rename to okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/DelegatingHttpsURLConnection.java
index fedf115..631a2ae 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/DelegatingHttpsURLConnection.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/DelegatingHttpsURLConnection.java
@@ -14,7 +14,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.huc;
import com.squareup.okhttp.Handshake;
import java.io.IOException;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
similarity index 73%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
rename to okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
index 32be0be..04ac552 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
@@ -15,7 +15,7 @@
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.huc;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.Handshake;
@@ -25,8 +25,15 @@
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpDate;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.HttpMethod;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.RetryableSink;
+import com.squareup.okhttp.internal.http.StatusLine;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -40,18 +47,17 @@
import java.net.URL;
import java.security.Permission;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
import okio.BufferedSink;
-import okio.ByteString;
import okio.Sink;
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
-
/**
* This implementation uses HttpEngine to send requests and receive responses.
* This class may use multiple HttpEngines to follow redirects, authentication
@@ -65,12 +71,8 @@
* header fields, request method, etc.) are immutable.
*/
public class HttpURLConnectionImpl extends HttpURLConnection {
-
- /**
- * How many redirects should we follow? Chrome follows 21; Firefox, curl,
- * and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
- */
- public static final int MAX_REDIRECTS = 20;
+ private static final Set<String> METHODS = new LinkedHashSet<>(
+ Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH"));
final OkHttpClient client;
@@ -78,9 +80,11 @@
/** Like the superclass field of the same name, but a long and available on all platforms. */
private long fixedContentLength = -1;
- private int redirectionCount;
+ private int followUpCount;
protected IOException httpEngineFailure;
protected HttpEngine httpEngine;
+ /** Lazily created (with synthetic headers) on first call to getHeaders(). */
+ private Headers responseHeaders;
/**
* The most recently attempted route. This will be null if we haven't sent a
@@ -111,10 +115,7 @@
// Calling disconnect() before a connection exists should have no effect.
if (httpEngine == null) return;
- try {
- httpEngine.disconnect();
- } catch (IOException ignored) {
- }
+ httpEngine.disconnect();
// 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
@@ -130,8 +131,9 @@
@Override public final InputStream getErrorStream() {
try {
HttpEngine response = getResponse();
- if (response.hasResponseBody() && response.getResponse().code() >= HTTP_BAD_REQUEST) {
- return response.getResponseBodyBytes();
+ if (HttpEngine.hasBody(response.getResponse())
+ && response.getResponse().code() >= HTTP_BAD_REQUEST) {
+ return response.getResponse().body().byteStream();
}
return null;
} catch (IOException e) {
@@ -139,13 +141,38 @@
}
}
+ private Headers getHeaders() throws IOException {
+ if (responseHeaders == null) {
+ Response response = getResponse().getResponse();
+ Headers headers = response.headers();
+
+ responseHeaders = headers.newBuilder()
+ .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response))
+ .build();
+ }
+ return responseHeaders;
+ }
+
+ private static String responseSourceHeader(Response response) {
+ if (response.networkResponse() == null) {
+ if (response.cacheResponse() == null) {
+ return "NONE";
+ }
+ return "CACHE " + response.code();
+ }
+ if (response.cacheResponse() == null) {
+ return "NETWORK " + response.code();
+ }
+ return "CONDITIONAL_CACHE " + response.networkResponse().code();
+ }
+
/**
* Returns the value of the field at {@code position}. Returns null if there
* are fewer than {@code position} headers.
*/
@Override public final String getHeaderField(int position) {
try {
- return getResponse().getResponse().headers().value(position);
+ return getHeaders().value(position);
} catch (IOException e) {
return null;
}
@@ -158,8 +185,9 @@
*/
@Override public final String getHeaderField(String fieldName) {
try {
- Response response = getResponse().getResponse();
- return fieldName == null ? response.statusLine() : response.headers().get(fieldName);
+ return fieldName == null
+ ? StatusLine.get(getResponse().getResponse()).toString()
+ : getHeaders().get(fieldName);
} catch (IOException e) {
return null;
}
@@ -167,7 +195,7 @@
@Override public final String getHeaderFieldKey(int position) {
try {
- return getResponse().getResponse().headers().name(position);
+ return getHeaders().name(position);
} catch (IOException e) {
return null;
}
@@ -175,8 +203,8 @@
@Override public final Map<String, List<String>> getHeaderFields() {
try {
- Response response = getResponse().getResponse();
- return OkHeaders.toMultimap(response.headers(), response.statusLine());
+ return OkHeaders.toMultimap(getHeaders(),
+ StatusLine.get(getResponse().getResponse()).toString());
} catch (IOException e) {
return Collections.emptyMap();
}
@@ -206,11 +234,7 @@
throw new FileNotFoundException(url.toString());
}
- InputStream result = response.getResponseBodyBytes();
- if (result == null) {
- throw new ProtocolException("No response body exists; responseCode=" + getResponseCode());
- }
- return result;
+ return response.getResponse().body().byteStream();
}
@Override public final OutputStream getOutputStream() throws IOException {
@@ -246,6 +270,11 @@
client.setConnectTimeout(timeoutMillis, TimeUnit.MILLISECONDS);
}
+ @Override
+ public void setInstanceFollowRedirects(boolean followRedirects) {
+ client.setFollowRedirects(followRedirects);
+ }
+
@Override public int getConnectTimeout() {
return client.getConnectTimeout();
}
@@ -271,11 +300,11 @@
if (method.equals("GET")) {
// they are requesting a stream to write to. This implies a POST method
method = "POST";
- } else if (!HttpMethod.hasRequestBody(method)) {
- // If the request method is neither POST nor PUT nor PATCH, then you're not writing
+ } else if (!HttpMethod.permitsRequestBody(method)) {
throw new ProtocolException(method + " does not support writing");
}
}
+ // If the user set content length to zero, we know there will not be a request body.
httpEngine = newHttpEngine(method, null, null, null);
} catch (IOException e) {
httpEngineFailure = e;
@@ -289,12 +318,13 @@
.url(getURL())
.method(method, null /* No body; that's passed separately. */);
Headers headers = requestHeaders.build();
- for (int i = 0; i < headers.size(); i++) {
+ for (int i = 0, size = headers.size(); i < size; i++) {
builder.addHeader(headers.name(i), headers.value(i));
}
boolean bufferRequestBody = false;
- if (HttpMethod.hasRequestBody(method)) {
+ if (HttpMethod.permitsRequestBody(method)) {
+ // Specify how the request body is terminated.
if (fixedContentLength != -1) {
builder.header("Content-Length", Long.toString(fixedContentLength));
} else if (chunkLength > 0) {
@@ -302,18 +332,32 @@
} else {
bufferRequestBody = true;
}
+
+ // Add a content type for the request body, if one isn't already present.
+ if (headers.get("Content-Type") == null) {
+ builder.header("Content-Type", "application/x-www-form-urlencoded");
+ }
+ }
+
+ if (headers.get("User-Agent") == null) {
+ builder.header("User-Agent", defaultUserAgent());
}
Request request = builder.build();
// If we're currently not using caches, make sure the engine's client doesn't have one.
OkHttpClient engineClient = client;
- if (engineClient.getOkResponseCache() != null && !getUseCaches()) {
- engineClient = client.clone().setOkResponseCache(null);
+ if (Internal.instance.internalCache(engineClient) != null && !getUseCaches()) {
+ engineClient = client.clone().setCache(null);
}
- return new HttpEngine(engineClient, request, bufferRequestBody, connection, null, requestBody,
- priorResponse);
+ return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null,
+ requestBody, priorResponse);
+ }
+
+ private String defaultUserAgent() {
+ String agent = System.getProperty("http.agent");
+ return agent != null ? agent : ("Java" + System.getProperty("java.version"));
}
/**
@@ -334,27 +378,26 @@
}
Response response = httpEngine.getResponse();
+ Request followUp = httpEngine.followUpRequest();
- Retry retry = processResponseHeaders();
- if (retry == Retry.NONE) {
+ if (followUp == null) {
httpEngine.releaseConnection();
return httpEngine;
}
- // The first request was insufficient. Prepare for another...
- String retryMethod = method;
- Sink requestBody = httpEngine.getRequestBody();
+ if (++followUpCount > HttpEngine.MAX_FOLLOW_UPS) {
+ throw new ProtocolException("Too many follow-up requests: " + followUpCount);
+ }
- // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM
- // redirect should keep the same method, Chrome, Firefox and the
- // RI all issue GETs when following any redirect.
- int responseCode = httpEngine.getResponse().code();
- if (responseCode == HTTP_MULT_CHOICE
- || responseCode == HTTP_MOVED_PERM
- || responseCode == HTTP_MOVED_TEMP
- || responseCode == HTTP_SEE_OTHER) {
- retryMethod = "GET";
- requestHeaders.removeAll("Content-Length");
+ // The first request was insufficient. Prepare for another...
+ url = followUp.url();
+ requestHeaders = followUp.headers().newBuilder();
+
+ // Although RFC 2616 10.3.2 specifies that a HTTP_MOVED_PERM redirect
+ // should keep the same method, Chrome, Firefox and the RI all issue GETs
+ // when following any redirect.
+ Sink requestBody = httpEngine.getRequestBody();
+ if (!followUp.method().equals(method)) {
requestBody = null;
}
@@ -362,12 +405,12 @@
throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
}
- if (retry == Retry.DIFFERENT_CONNECTION) {
+ if (!httpEngine.sameConnection(followUp.url())) {
httpEngine.releaseConnection();
}
Connection connection = httpEngine.close();
- httpEngine = newHttpEngine(retryMethod, connection, (RetryableSink) requestBody,
+ httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody,
response);
}
}
@@ -402,78 +445,6 @@
}
}
- enum Retry {
- NONE,
- SAME_CONNECTION,
- DIFFERENT_CONNECTION
- }
-
- /**
- * Returns the retry action to take for the current response headers. The
- * headers, proxy and target URL for this connection may be adjusted to
- * prepare for a follow up request.
- */
- private Retry processResponseHeaders() throws IOException {
- Connection connection = httpEngine.getConnection();
- Proxy selectedProxy = connection != null
- ? connection.getRoute().getProxy()
- : client.getProxy();
- final int responseCode = getResponseCode();
- switch (responseCode) {
- case HTTP_PROXY_AUTH:
- if (selectedProxy.type() != Proxy.Type.HTTP) {
- throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
- }
- // fall-through
- case HTTP_UNAUTHORIZED:
- Request successorRequest = HttpAuthenticator.processAuthHeader(client.getAuthenticator(),
- httpEngine.getResponse(), selectedProxy);
- if (successorRequest == null) return Retry.NONE;
- requestHeaders = successorRequest.getHeaders().newBuilder();
- return Retry.SAME_CONNECTION;
-
- case HTTP_MULT_CHOICE:
- case HTTP_MOVED_PERM:
- case HTTP_MOVED_TEMP:
- case HTTP_SEE_OTHER:
- case HTTP_TEMP_REDIRECT:
- if (!getInstanceFollowRedirects()) {
- return Retry.NONE;
- }
- if (++redirectionCount > MAX_REDIRECTS) {
- throw new ProtocolException("Too many redirects: " + redirectionCount);
- }
- if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
- // "If the 307 status code is received in response to a request other than GET or HEAD,
- // the user agent MUST NOT automatically redirect the request"
- return Retry.NONE;
- }
- String location = getHeaderField("Location");
- if (location == null) {
- return Retry.NONE;
- }
- URL previousUrl = url;
- url = new URL(previousUrl, location);
- if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
- return Retry.NONE; // Don't follow redirects to unsupported protocols.
- }
- boolean sameProtocol = previousUrl.getProtocol().equals(url.getProtocol());
- if (!sameProtocol && !client.getFollowProtocolRedirects()) {
- return Retry.NONE; // This client doesn't follow redirects across protocols.
- }
- boolean sameHost = previousUrl.getHost().equals(url.getHost());
- boolean samePort = getEffectivePort(previousUrl) == getEffectivePort(url);
- if (sameHost && samePort && sameProtocol) {
- return Retry.SAME_CONNECTION;
- } else {
- return Retry.DIFFERENT_CONNECTION;
- }
-
- default:
- return Retry.NONE;
- }
- }
-
/**
* Returns true if either:
* <ul>
@@ -493,7 +464,7 @@
}
@Override public String getResponseMessage() throws IOException {
- return getResponse().getResponse().statusMessage();
+ return getResponse().getResponse().message();
}
@Override public final int getResponseCode() throws IOException {
@@ -566,13 +537,13 @@
* defined in {@link Protocol OkHttp's protocol enumeration}.
*/
private void setProtocols(String protocolsString, boolean append) {
- List<Protocol> protocolsList = new ArrayList<Protocol>();
+ List<Protocol> protocolsList = new ArrayList<>();
if (append) {
protocolsList.addAll(client.getProtocols());
}
for (String protocol : protocolsString.split(",", -1)) {
try {
- protocolsList.add(Protocol.find(ByteString.encodeUtf8(protocol)));
+ protocolsList.add(Protocol.get(protocol));
} catch (IOException e) {
throw new IllegalStateException(e);
}
@@ -581,9 +552,8 @@
}
@Override public void setRequestMethod(String method) throws ProtocolException {
- if (!HttpMethod.METHODS.contains(method)) {
- throw new ProtocolException(
- "Expected one of " + HttpMethod.METHODS + " but was " + method);
+ if (!METHODS.contains(method)) {
+ throw new ProtocolException("Expected one of " + METHODS + " but was " + method);
}
this.method = method;
}
@@ -592,8 +562,7 @@
setFixedLengthStreamingMode((long) contentLength);
}
- // @Override Don't override: this overload method doesn't exist prior to Java 1.7.
- public void setFixedLengthStreamingMode(long contentLength) {
+ @Override public void setFixedLengthStreamingMode(long contentLength) {
if (super.connected) throw new IllegalStateException("Already connected");
if (chunkLength > 0) throw new IllegalStateException("Already in chunked mode");
if (contentLength < 0) throw new IllegalArgumentException("contentLength < 0");
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
similarity index 86%
rename from okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
rename to okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
index 232e1ca..75f7158 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpsURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
@@ -14,7 +14,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.huc;
import com.squareup.okhttp.Handshake;
import com.squareup.okhttp.OkHttpClient;
@@ -63,7 +63,19 @@
return delegate.client.getSslSocketFactory();
}
+ // ANDROID-BEGIN
+ // @Override public long getContentLengthLong() {
+ // return delegate.getContentLengthLong();
+ // }
+ // ANDROID-END
+
@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
}
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
new file mode 100644
index 0000000..a7dc44b
--- /dev/null
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
@@ -0,0 +1,167 @@
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static okio.Okio.buffer;
+import static okio.Okio.source;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class OkUrlFactoryTest {
+ @Rule public MockWebServerRule serverRule = new MockWebServerRule();
+ @Rule public TemporaryFolder cacheFolder = new TemporaryFolder();
+
+ private MockWebServer server;
+ private OkUrlFactory factory;
+
+ @Before public void setUp() throws IOException {
+ server = serverRule.get();
+
+ OkHttpClient client = new OkHttpClient();
+ client.setCache(new Cache(cacheFolder.getRoot(), 10 * 1024 * 1024));
+ factory = new OkUrlFactory(client);
+ }
+
+ /**
+ * Response code 407 should only come from proxy servers. Android's client
+ * throws if it is sent by an origin server.
+ */
+ @Test public void originServerSends407() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(407));
+
+ HttpURLConnection conn = factory.open(server.getUrl("/"));
+ try {
+ conn.getResponseCode();
+ fail();
+ } catch (IOException ignored) {
+ }
+ }
+
+ @Test public void networkResponseSourceHeader() throws Exception {
+ server.enqueue(new MockResponse().setBody("Isla Sorna"));
+
+ HttpURLConnection connection = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection, "NETWORK 200");
+ assertResponseBody(connection, "Isla Sorna");
+ }
+
+ @Test public void networkFailureResponseSourceHeader() throws Exception {
+ server.enqueue(new MockResponse().setResponseCode(404));
+
+ HttpURLConnection connection = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection, "NETWORK 404");
+ }
+
+ @Test public void conditionalCacheHitResponseSourceHeaders() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("Isla Nublar"));
+ server.enqueue(new MockResponse().setResponseCode(304));
+
+ HttpURLConnection connection1 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection1, "NETWORK 200");
+ assertResponseBody(connection1, "Isla Nublar");
+
+ HttpURLConnection connection2 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection2, "CONDITIONAL_CACHE 304");
+ assertResponseBody(connection2, "Isla Nublar");
+ }
+
+ @Test public void conditionalCacheMissResponseSourceHeaders() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(0, TimeUnit.SECONDS))
+ .addHeader("Cache-Control: max-age=0")
+ .setBody("Isla Nublar"));
+ server.enqueue(new MockResponse().setBody("Isla Sorna"));
+
+ HttpURLConnection connection1 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection1, "NETWORK 200");
+ assertResponseBody(connection1, "Isla Nublar");
+
+ HttpURLConnection connection2 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection2, "CONDITIONAL_CACHE 200");
+ assertResponseBody(connection2, "Isla Sorna");
+ }
+
+ @Test public void cacheResponseSourceHeaders() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Expires: " + formatDate(2, TimeUnit.HOURS))
+ .setBody("Isla Nublar"));
+
+ HttpURLConnection connection1 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection1, "NETWORK 200");
+ assertResponseBody(connection1, "Isla Nublar");
+
+ HttpURLConnection connection2 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection2, "CACHE 200");
+ assertResponseBody(connection2, "Isla Nublar");
+ }
+
+ @Test public void noneResponseSourceHeaders() throws Exception {
+ server.enqueue(new MockResponse().setBody("Isla Nublar"));
+
+ HttpURLConnection connection1 = factory.open(server.getUrl("/"));
+ assertResponseHeader(connection1, "NETWORK 200");
+ assertResponseBody(connection1, "Isla Nublar");
+
+ HttpURLConnection connection2 = factory.open(server.getUrl("/"));
+ connection2.setRequestProperty("Cache-Control", "only-if-cached");
+ assertResponseHeader(connection2, "NONE");
+ }
+
+ @Test
+ public void setInstanceFollowRedirectsFalse() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(302)
+ .addHeader("Location: /b")
+ .setBody("A"));
+ server.enqueue(new MockResponse()
+ .setBody("B"));
+
+ HttpURLConnection connection = factory.open(server.getUrl("/a"));
+ connection.setInstanceFollowRedirects(false);
+ assertResponseBody(connection, "A");
+ assertResponseCode(connection, 302);
+ }
+
+ private void assertResponseBody(HttpURLConnection connection, String expected) throws Exception {
+ String actual = buffer(source(connection.getInputStream())).readString(US_ASCII);
+ assertEquals(expected, actual);
+ }
+
+ private void assertResponseHeader(HttpURLConnection connection, String expected) {
+ final String headerFieldPrefix = Platform.get().getPrefix();
+ assertEquals(expected, connection.getHeaderField(headerFieldPrefix + "-Response-Source"));
+ }
+
+ private void assertResponseCode(HttpURLConnection connection, int expected) throws IOException {
+ assertEquals(expected, connection.getResponseCode());
+ }
+
+ private static String formatDate(long delta, TimeUnit timeUnit) {
+ return formatDate(new Date(System.currentTimeMillis() + timeUnit.toMillis(delta)));
+ }
+
+ private static String formatDate(Date date) {
+ DateFormat rfc1123 = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US);
+ rfc1123.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return rfc1123.format(date);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
similarity index 71%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
index 541351a..c46fd07 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpResponseCacheTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
@@ -14,36 +14,41 @@
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp;
-import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.HttpResponseCache;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.OkResponseCache;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import com.squareup.okhttp.mockwebserver.rule.MockWebServerRule;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.GzipSink;
+import okio.Okio;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
import java.io.BufferedReader;
-import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
-import java.net.CacheRequest;
import java.net.CookieHandler;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.ResponseCache;
-import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.security.Principal;
@@ -53,24 +58,10 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
-import java.util.HashMap;
-import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
-import java.util.UUID;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.zip.GZIPOutputStream;
-import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSession;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_END;
import static org.junit.Assert.assertEquals;
@@ -81,11 +72,8 @@
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
-/**
- * Android's HttpResponseCacheTest. This tests both the {@link HttpResponseCache} implementation and
- * the behavior of {@link com.squareup.okhttp.OkResponseCache} classes generally.
- */
-public final class HttpResponseCacheTest {
+/** Test caching with {@link OkUrlFactory}. */
+public final class UrlConnectionCacheTest {
private static final HostnameVerifier NULL_HOSTNAME_VERIFIER = new HostnameVerifier() {
@Override public boolean verify(String s, SSLSession sslSession) {
return true;
@@ -94,45 +82,32 @@
private static final SSLContext sslContext = SslContextBuilder.localhost();
- private final OkHttpClient client = new OkHttpClient();
- private MockWebServer server = new MockWebServer();
- private MockWebServer server2 = new MockWebServer();
- private HttpResponseCache cache;
+ @Rule public TemporaryFolder cacheRule = new TemporaryFolder();
+ @Rule public MockWebServerRule serverRule = new MockWebServerRule();
+ @Rule public MockWebServerRule server2Rule = new MockWebServerRule();
+
+ private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
+ private MockWebServer server;
+ private MockWebServer server2;
+ private Cache cache;
private final CookieManager cookieManager = new CookieManager();
@Before public void setUp() throws Exception {
- String tmp = System.getProperty("java.io.tmpdir");
- File cacheDir = new File(tmp, "HttpCache-" + UUID.randomUUID());
- cache = new HttpResponseCache(cacheDir, Integer.MAX_VALUE);
- ResponseCache.setDefault(cache);
+ server = serverRule.get();
+ server.setProtocolNegotiationEnabled(false);
+ server2 = server2Rule.get();
+ cache = new Cache(cacheRule.getRoot(), Integer.MAX_VALUE);
+ client.client().setCache(cache);
CookieHandler.setDefault(cookieManager);
- server.setNpnEnabled(false);
}
@After public void tearDown() throws Exception {
- server.shutdown();
- server2.shutdown();
ResponseCache.setDefault(null);
- cache.delete();
CookieHandler.setDefault(null);
}
- private HttpURLConnection openConnection(URL url) {
- return client.open(url);
- }
-
@Test public void responseCacheAccessWithOkHttpMember() throws IOException {
- ResponseCache.setDefault(null);
- client.setResponseCache(cache);
- assertSame(cache, client.getOkResponseCache());
- assertNull(client.getResponseCache());
- }
-
- @Test public void responseCacheAccessWithGlobalDefault() throws IOException {
- ResponseCache.setDefault(cache);
- client.setResponseCache(null);
- assertNull(client.getOkResponseCache());
- assertNull(client.getResponseCache());
+ assertSame(cache, client.client().getCache());
}
/**
@@ -140,81 +115,77 @@
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.4
*/
@Test public void responseCachingByResponseCode() throws Exception {
- // Test each documented HTTP/1.1 code, plus the first unused value in each range.
- // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ // Test each documented HTTP/1.1 code, plus the first unused value in each range.
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
- // We can't test 100 because it's not really a response.
- // assertCached(false, 100);
- assertCached(false, 101);
- assertCached(false, 102);
- assertCached(true, 200);
- assertCached(false, 201);
- assertCached(false, 202);
- assertCached(true, 203);
- assertCached(false, 204);
- assertCached(false, 205);
- assertCached(false, 206); // we don't cache partial responses
- assertCached(false, 207);
- assertCached(true, 300);
- assertCached(true, 301);
- for (int i = 302; i <= 308; ++i) {
- assertCached(false, i);
- }
- for (int i = 400; i <= 406; ++i) {
- assertCached(false, i);
- }
- // (See test_responseCaching_407.)
- assertCached(false, 408);
- assertCached(false, 409);
- // (See test_responseCaching_410.)
- for (int i = 411; i <= 418; ++i) {
- assertCached(false, i);
- }
- for (int i = 500; i <= 506; ++i) {
- assertCached(false, i);
- }
- }
+ // We can't test 100 because it's not really a response.
+ // assertCached(false, 100);
+ assertCached(false, 101);
+ assertCached(false, 102);
+ assertCached(true, 200);
+ assertCached(false, 201);
+ assertCached(false, 202);
+ assertCached(true, 203);
+ assertCached(true, 204);
+ assertCached(false, 205);
+ assertCached(false, 206); //Electing to not cache partial responses
+ assertCached(false, 207);
+ assertCached(true, 300);
+ assertCached(true, 301);
+ assertCached(true, 302);
+ assertCached(false, 303);
+ assertCached(false, 304);
+ assertCached(false, 305);
+ assertCached(false, 306);
+ assertCached(true, 307);
+ assertCached(true, 308);
+ assertCached(false, 400);
+ assertCached(false, 401);
+ assertCached(false, 402);
+ assertCached(false, 403);
+ assertCached(true, 404);
+ assertCached(true, 405);
+ assertCached(false, 406);
+ assertCached(false, 408);
+ assertCached(false, 409);
+ // the HTTP spec permits caching 410s, but the RI doesn't.
+ assertCached(true, 410);
+ assertCached(false, 411);
+ assertCached(false, 412);
+ assertCached(false, 413);
+ assertCached(true, 414);
+ assertCached(false, 415);
+ assertCached(false, 416);
+ assertCached(false, 417);
+ assertCached(false, 418);
- /**
- * Response code 407 should only come from proxy servers. Android's client
- * throws if it is sent by an origin server.
- */
- @Test public void originServerSends407() throws Exception {
- server.enqueue(new MockResponse().setResponseCode(407));
- server.play();
-
- URL url = server.getUrl("/");
- HttpURLConnection conn = openConnection(url);
- try {
- conn.getResponseCode();
- fail();
- } catch (IOException expected) {
- }
- }
-
- @Test public void responseCaching_410() throws Exception {
- // the HTTP spec permits caching 410s, but the RI doesn't.
- assertCached(true, 410);
+ assertCached(false, 500);
+ assertCached(true, 501);
+ assertCached(false, 502);
+ assertCached(false, 503);
+ assertCached(false, 504);
+ assertCached(false, 505);
+ assertCached(false, 506);
}
private void assertCached(boolean shouldPut, int responseCode) throws Exception {
server = new MockWebServer();
- MockResponse response =
- new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
- .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
- .setResponseCode(responseCode)
- .setBody("ABCDE")
- .addHeader("WWW-Authenticate: challenge");
+ MockResponse response = new MockResponse()
+ .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
+ .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
+ .setResponseCode(responseCode)
+ .setBody("ABCDE")
+ .addHeader("WWW-Authenticate: challenge");
if (responseCode == HttpURLConnection.HTTP_PROXY_AUTH) {
response.addHeader("Proxy-Authenticate: Basic realm=\"protected area\"");
} else if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) {
response.addHeader("WWW-Authenticate: Basic realm=\"protected area\"");
}
server.enqueue(response);
- server.play();
+ server.start();
URL url = server.getUrl("/");
- HttpURLConnection conn = openConnection(url);
+ HttpURLConnection conn = client.open(url);
assertEquals(responseCode, conn.getResponseCode());
// exhaust the content stream
@@ -230,66 +201,6 @@
server.shutdown(); // tearDown() isn't sufficient; this test starts multiple servers
}
- /**
- * Test that we can interrogate the response when the cache is being
- * populated. http://code.google.com/p/android/issues/detail?id=7787
- */
- @Test public void responseCacheCallbackApis() throws Exception {
- final String body = "ABCDE";
- final AtomicInteger cacheCount = new AtomicInteger();
-
- server.enqueue(new MockResponse()
- .setStatus("HTTP/1.1 200 Fantastic")
- .addHeader("Content-Type: text/plain")
- .addHeader("fgh: ijk")
- .setBody(body));
- server.play();
-
- client.setOkResponseCache(new AbstractOkResponseCache() {
- @Override public CacheRequest put(Response response) throws IOException {
- assertEquals(server.getUrl("/"), response.request().url());
- assertEquals(200, response.code());
- assertNull(response.body());
- assertEquals("5", response.header("Content-Length"));
- assertEquals("text/plain", response.header("Content-Type"));
- assertEquals("ijk", response.header("fgh"));
- cacheCount.incrementAndGet();
- return null;
- }
- });
-
- URL url = server.getUrl("/");
- HttpURLConnection connection = openConnection(url);
- assertEquals(body, readAscii(connection));
- assertEquals(1, cacheCount.get());
- }
-
- /** Don't explode if the cache returns a null body. http://b/3373699 */
- @Test public void responseCacheReturnsNullOutputStream() throws Exception {
- final AtomicBoolean aborted = new AtomicBoolean();
- client.setOkResponseCache(new AbstractOkResponseCache() {
- @Override public CacheRequest put(Response response) throws IOException {
- return new CacheRequest() {
- @Override public void abort() {
- aborted.set(true);
- }
-
- @Override public OutputStream getBody() throws IOException {
- return null;
- }
- };
- }
- });
-
- server.enqueue(new MockResponse().setBody("abcdef"));
- server.play();
-
- HttpURLConnection connection = client.open(server.getUrl("/"));
- assertEquals("abc", readAscii(connection, 3));
- connection.getInputStream().close();
- assertFalse(aborted.get()); // The best behavior is ambiguous, but RI 6 doesn't abort here
- }
-
@Test public void responseCachingAndInputStreamSkipWithFixedLength() throws IOException {
testResponseCaching(TransferKind.FIXED_LENGTH);
}
@@ -313,10 +224,9 @@
.setStatus("HTTP/1.1 200 Fantastic");
transferKind.setBody(response, "I love puppies but hate spiders", 1);
server.enqueue(response);
- server.play();
// Make sure that calling skip() doesn't omit bytes from the cache.
- HttpURLConnection urlConnection = openConnection(server.getUrl("/"));
+ HttpURLConnection urlConnection = client.open(server.getUrl("/"));
InputStream in = urlConnection.getInputStream();
assertEquals("I love ", readAscii(urlConnection, "I love ".length()));
reliableSkip(in, "puppies but hate ".length());
@@ -326,7 +236,7 @@
assertEquals(1, cache.getWriteSuccessCount());
assertEquals(0, cache.getWriteAbortCount());
- urlConnection = openConnection(server.getUrl("/")); // cached!
+ urlConnection = client.open(server.getUrl("/")); // cached!
in = urlConnection.getInputStream();
assertEquals("I love puppies but hate spiders",
readAscii(urlConnection, "I love puppies but hate spiders".length()));
@@ -346,7 +256,6 @@
server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
- server.play();
HttpsURLConnection c1 = (HttpsURLConnection) client.open(server.getUrl("/"));
c1.setSSLSocketFactory(sslContext.getSocketFactory());
@@ -385,12 +294,11 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("ABC", readAscii(connection));
- connection = openConnection(server.getUrl("/")); // cached!
+ connection = client.open(server.getUrl("/")); // cached!
assertEquals("ABC", readAscii(connection));
assertEquals(4, cache.getRequestCount()); // 2 requests + 2 redirects
@@ -403,20 +311,19 @@
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: /foo"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
- assertEquals("ABC", readAscii(openConnection(server.getUrl("/foo"))));
+ assertEquals("ABC", readAscii(client.open(server.getUrl("/foo"))));
RecordedRequest request1 = server.takeRequest();
assertEquals("GET /foo HTTP/1.1", request1.getRequestLine());
assertEquals(0, request1.getSequenceNumber());
- assertEquals("ABC", readAscii(openConnection(server.getUrl("/bar"))));
+ assertEquals("ABC", readAscii(client.open(server.getUrl("/bar"))));
RecordedRequest request2 = server.takeRequest();
assertEquals("GET /bar HTTP/1.1", request2.getRequestLine());
assertEquals(1, request2.getSequenceNumber());
// an unrelated request should reuse the pooled connection
- assertEquals("DEF", readAscii(openConnection(server.getUrl("/baz"))));
+ assertEquals("DEF", readAscii(client.open(server.getUrl("/baz"))));
RecordedRequest request3 = server.takeRequest();
assertEquals("GET /baz HTTP/1.1", request3.getRequestLine());
assertEquals(2, request3.getSequenceNumber());
@@ -432,10 +339,9 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server.enqueue(new MockResponse().setBody("DEF"));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
HttpsURLConnection connection1 = (HttpsURLConnection) client.open(server.getUrl("/"));
assertEquals("ABC", readAscii(connection1));
@@ -465,16 +371,14 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setBody("ABC"));
server2.enqueue(new MockResponse().setBody("DEF"));
- server2.play();
server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.setResponseCode(HttpURLConnection.HTTP_MOVED_PERM)
.addHeader("Location: " + server2.getUrl("/")));
- server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
+ client.client().setSslSocketFactory(sslContext.getSocketFactory());
+ client.client().setHostnameVerifier(NULL_HOSTNAME_VERIFIER);
HttpURLConnection connection1 = client.open(server.getUrl("/"));
assertEquals("ABC", readAscii(connection1));
@@ -487,25 +391,6 @@
assertEquals(2, cache.getHitCount());
}
- @Test public void responseCacheRequestHeaders() throws IOException, URISyntaxException {
- server.enqueue(new MockResponse().setBody("ABC"));
- server.play();
-
- final AtomicReference<Request> requestRef = new AtomicReference<Request>();
- client.setOkResponseCache(new AbstractOkResponseCache() {
- @Override public Response get(Request request) throws IOException {
- requestRef.set(request);
- return null;
- }
- });
-
- URL url = server.getUrl("/");
- URLConnection urlConnection = openConnection(url);
- urlConnection.addRequestProperty("A", "android");
- readAscii(urlConnection);
- assertEquals(Arrays.asList("android"), requestRef.get().headers("A"));
- }
-
@Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
@@ -525,10 +410,9 @@
transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 16);
server.enqueue(truncateViolently(response, 16));
server.enqueue(new MockResponse().setBody("Request #2"));
- server.play();
BufferedReader reader = new BufferedReader(
- new InputStreamReader(openConnection(server.getUrl("/")).getInputStream()));
+ new InputStreamReader(client.open(server.getUrl("/")).getInputStream()));
assertEquals("ABCDE", reader.readLine());
try {
reader.readLine();
@@ -540,7 +424,7 @@
assertEquals(1, cache.getWriteAbortCount());
assertEquals(0, cache.getWriteSuccessCount());
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
assertEquals("Request #2", readAscii(connection));
assertEquals(1, cache.getWriteAbortCount());
assertEquals(1, cache.getWriteSuccessCount());
@@ -564,9 +448,8 @@
transferKind.setBody(response, "ABCDE\nFGHIJKLMNOPQRSTUVWXYZ", 1024);
server.enqueue(response);
server.enqueue(new MockResponse().setBody("Request #2"));
- server.play();
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
InputStream in = connection.getInputStream();
assertEquals("ABCDE", readAscii(connection, 5));
in.close();
@@ -578,7 +461,7 @@
assertEquals(1, cache.getWriteAbortCount());
assertEquals(0, cache.getWriteSuccessCount());
- connection = openConnection(server.getUrl("/"));
+ connection = client.open(server.getUrl("/"));
assertEquals("Request #2", readAscii(connection));
assertEquals(1, cache.getWriteAbortCount());
assertEquals(1, cache.getWriteSuccessCount());
@@ -593,11 +476,10 @@
new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.SECONDS))
.addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
.setBody("A"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- URLConnection connection = openConnection(url);
+ assertEquals("A", readAscii(client.open(url)));
+ URLConnection connection = client.open(url);
assertEquals("A", readAscii(connection));
assertNull(connection.getHeaderField("Warning"));
}
@@ -611,8 +493,7 @@
RecordedRequest conditionalRequest = assertConditionallyCached(
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Date: " + formatDate(-15, TimeUnit.SECONDS)));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void defaultExpirationDateFullyCachedForMoreThan24Hours() throws Exception {
@@ -623,10 +504,9 @@
server.enqueue(new MockResponse().addHeader("Last-Modified: " + formatDate(-105, TimeUnit.DAYS))
.addHeader("Date: " + formatDate(-5, TimeUnit.DAYS))
.setBody("A"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ URLConnection connection = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection));
assertEquals("113 HttpURLConnection \"Heuristic expiration\"",
connection.getHeaderField("Warning"));
@@ -638,11 +518,10 @@
.addHeader("Date: " + formatDate(-5, TimeUnit.SECONDS))
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/?foo=bar");
- assertEquals("A", readAscii(openConnection(url)));
- assertEquals("B", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
+ assertEquals("B", readAscii(client.open(url)));
}
@Test public void expirationDateInThePastWithLastModifiedHeader() throws Exception {
@@ -650,8 +529,7 @@
RecordedRequest conditionalRequest = assertConditionallyCached(
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void expirationDateInThePastWithNoLastModifiedHeader() throws Exception {
@@ -674,8 +552,7 @@
new MockResponse().addHeader("Date: " + formatDate(-120, TimeUnit.SECONDS))
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Cache-Control: max-age=60"));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void maxAgeInThePastWithDateHeaderButNoLastModifiedHeader() throws Exception {
@@ -755,16 +632,17 @@
server.enqueue(new MockResponse().addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("X-Response-ID: 1"));
server.enqueue(new MockResponse().addHeader("X-Response-ID: 2"));
- server.play();
URL url = server.getUrl("/");
- HttpURLConnection request1 = openConnection(url);
+ HttpURLConnection request1 = client.open(url);
request1.setRequestMethod(requestMethod);
addRequestBodyIfNecessary(requestMethod, request1);
+ request1.getInputStream().close();
assertEquals("1", request1.getHeaderField("X-Response-ID"));
- URLConnection request2 = openConnection(url);
+ URLConnection request2 = client.open(url);
+ request2.getInputStream().close();
if (expectCached) {
assertEquals("1", request2.getHeaderField("X-Response-ID"));
} else {
@@ -792,18 +670,17 @@
new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B"));
server.enqueue(new MockResponse().setBody("C"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
- HttpURLConnection invalidate = openConnection(url);
+ HttpURLConnection invalidate = client.open(url);
invalidate.setRequestMethod(requestMethod);
addRequestBodyIfNecessary(requestMethod, invalidate);
assertEquals("B", readAscii(invalidate));
- assertEquals("C", readAscii(openConnection(url)));
+ assertEquals("C", readAscii(client.open(url)));
}
@Test public void postInvalidatesCacheWithUncacheableResponse() throws Exception {
@@ -814,24 +691,23 @@
new MockResponse().setBody("A").addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B").setResponseCode(500));
server.enqueue(new MockResponse().setBody("C"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
- HttpURLConnection invalidate = openConnection(url);
+ HttpURLConnection invalidate = client.open(url);
invalidate.setRequestMethod("POST");
addRequestBodyIfNecessary("POST", invalidate);
assertEquals("B", readAscii(invalidate));
- assertEquals("C", readAscii(openConnection(url)));
+ assertEquals("C", readAscii(client.open(url)));
}
@Test public void etag() throws Exception {
RecordedRequest conditionalRequest =
assertConditionallyCached(new MockResponse().addHeader("ETag: v1"));
- assertTrue(conditionalRequest.getHeaders().contains("If-None-Match: v1"));
+ assertEquals("v1", conditionalRequest.getHeader("If-None-Match"));
}
@Test public void etagAndExpirationDateInThePast() throws Exception {
@@ -840,9 +716,8 @@
new MockResponse().addHeader("ETag: v1")
.addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-None-Match: v1"));
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals("v1", conditionalRequest.getHeader("If-None-Match"));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void etagAndExpirationDateInTheFuture() throws Exception {
@@ -861,8 +736,7 @@
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Cache-Control: no-cache"));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void pragmaNoCache() throws Exception {
@@ -875,8 +749,7 @@
new MockResponse().addHeader("Last-Modified: " + lastModifiedDate)
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Pragma: no-cache"));
- List<String> headers = conditionalRequest.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals(lastModifiedDate, conditionalRequest.getHeader("If-Modified-Since"));
}
@Test public void cacheControlNoStore() throws Exception {
@@ -897,15 +770,14 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
.addHeader("Content-Range: bytes 1000-1001/2000"));
server.enqueue(new MockResponse().setBody("BB"));
- server.play();
URL url = server.getUrl("/");
- URLConnection range = openConnection(url);
+ URLConnection range = client.open(url);
range.addRequestProperty("Range", "bytes=1000-1001");
assertEquals("AA", readAscii(range));
- assertEquals("BB", readAscii(openConnection(url)));
+ assertEquals("BB", readAscii(client.open(url)));
}
@Test public void serverReturnsDocumentOlderThanCache() throws Exception {
@@ -914,12 +786,11 @@
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B")
.addHeader("Last-Modified: " + formatDate(-4, TimeUnit.HOURS)));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
+ assertEquals("A", readAscii(client.open(url)));
}
@Test public void nonIdentityEncodingAndConditionalCache() throws Exception {
@@ -936,23 +807,21 @@
private void assertNonIdentityEncodingCached(MockResponse response) throws Exception {
server.enqueue(
- response.setBody(gzip("ABCABCABC".getBytes("UTF-8"))).addHeader("Content-Encoding: gzip"));
+ response.setBody(gzip("ABCABCABC")).addHeader("Content-Encoding: gzip"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
-
// At least three request/response pairs are required because after the first request is cached
// a different execution path might be taken. Thus modifications to the cache applied during
// the second request might not be visible until another request is performed.
- assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
}
@Test public void notModifiedSpecifiesEncoding() throws Exception {
server.enqueue(new MockResponse()
- .setBody(gzip("ABCABCABC".getBytes("UTF-8")))
+ .setBody(gzip("ABCABCABC"))
.addHeader("Content-Encoding: gzip")
.addHeader("Last-Modified: " + formatDate(-2, TimeUnit.HOURS))
.addHeader("Expires: " + formatDate(-1, TimeUnit.HOURS)));
@@ -962,10 +831,22 @@
server.enqueue(new MockResponse()
.setBody("DEFDEFDEF"));
- server.play();
- assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("ABCABCABC", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("DEFDEFDEF", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("DEFDEFDEF", readAscii(client.open(server.getUrl("/"))));
+ }
+
+ /** https://github.com/square/okhttp/issues/947 */
+ @Test public void gzipAndVaryOnAcceptEncoding() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody(gzip("ABCABCABC"))
+ .addHeader("Content-Encoding: gzip")
+ .addHeader("Vary: Accept-Encoding")
+ .addHeader("Cache-Control: max-age=60"));
+ server.enqueue(new MockResponse().setBody("FAIL"));
+
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("ABCABCABC", readAscii(client.open(server.getUrl("/"))));
}
@Test public void conditionalCacheHitIsNotDoublePooled() throws Exception {
@@ -973,15 +854,14 @@
server.enqueue(new MockResponse()
.clearHeaders()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
ConnectionPool pool = ConnectionPool.getDefault();
pool.evictAll();
- client.setConnectionPool(pool);
+ client.client().setConnectionPool(pool);
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals(1, client.getConnectionPool().getConnectionCount());
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals(1, client.client().getConnectionPool().getConnectionCount());
}
@Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -997,10 +877,9 @@
.addHeader("Expires: " + formatDate(1, TimeUnit.HOURS)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "max-age=30");
assertEquals("B", readAscii(connection));
}
@@ -1011,10 +890,9 @@
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "min-fresh=120");
assertEquals("B", readAscii(connection));
}
@@ -1025,10 +903,9 @@
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "max-stale=180");
assertEquals("A", readAscii(connection));
assertEquals("110 HttpURLConnection \"Response is stale\"",
@@ -1041,19 +918,17 @@
.addHeader("Date: " + formatDate(-4, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "max-stale=180");
assertEquals("B", readAscii(connection));
}
@Test public void requestOnlyIfCachedWithNoResponseCached() throws IOException {
// (no responses enqueued)
- server.play();
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertGatewayTimeout(connection);
assertEquals(1, cache.getRequestCount());
@@ -1065,10 +940,9 @@
server.enqueue(new MockResponse().setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertEquals("A", readAscii(connection));
assertEquals(2, cache.getRequestCount());
@@ -1080,10 +954,9 @@
server.enqueue(new MockResponse().setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(-1, TimeUnit.MINUTES)));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertGatewayTimeout(connection);
assertEquals(2, cache.getRequestCount());
@@ -1093,10 +966,9 @@
@Test public void requestOnlyIfCachedWithUnhelpfulResponseCached() throws IOException {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertGatewayTimeout(connection);
assertEquals(2, cache.getRequestCount());
@@ -1111,11 +983,10 @@
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- URLConnection connection = openConnection(url);
+ assertEquals("A", readAscii(client.open(url)));
+ URLConnection connection = client.open(url);
connection.setRequestProperty("Cache-Control", "no-cache");
assertEquals("B", readAscii(connection));
}
@@ -1127,11 +998,10 @@
.addHeader("Cache-Control: max-age=60")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- URLConnection connection = openConnection(url);
+ assertEquals("A", readAscii(client.open(url)));
+ URLConnection connection = client.open(url);
connection.setRequestProperty("Pragma", "no-cache");
assertEquals("B", readAscii(connection));
}
@@ -1142,9 +1012,8 @@
String ifModifiedSinceDate = formatDate(-24, TimeUnit.HOURS);
RecordedRequest request =
assertClientSuppliedCondition(response, "If-Modified-Since", ifModifiedSinceDate);
- List<String> headers = request.getHeaders();
- assertTrue(headers.contains("If-Modified-Since: " + ifModifiedSinceDate));
- assertFalse(headers.contains("If-None-Match: v3"));
+ assertEquals(ifModifiedSinceDate, request.getHeader("If-Modified-Since"));
+ assertNull(request.getHeader("If-None-Match"));
}
@Test public void clientSuppliedIfNoneMatchSinceWithCachedResult() throws Exception {
@@ -1153,21 +1022,19 @@
.addHeader("Date: " + formatDate(-2, TimeUnit.MINUTES))
.addHeader("Cache-Control: max-age=0");
RecordedRequest request = assertClientSuppliedCondition(response, "If-None-Match", "v1");
- List<String> headers = request.getHeaders();
- assertTrue(headers.contains("If-None-Match: v1"));
- assertFalse(headers.contains("If-Modified-Since: " + lastModifiedDate));
+ assertEquals("v1", request.getHeader("If-None-Match"));
+ assertNull(request.getHeader("If-Modified-Since"));
}
private RecordedRequest assertClientSuppliedCondition(MockResponse seed, String conditionName,
String conditionValue) throws Exception {
server.enqueue(seed.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
- HttpURLConnection connection = openConnection(url);
+ HttpURLConnection connection = client.open(url);
connection.addRequestProperty(conditionName, conditionValue);
assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
assertEquals("", readAscii(connection));
@@ -1184,10 +1051,9 @@
*/
@Test public void setIfModifiedSince() throws Exception {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection = openConnection(url);
+ URLConnection connection = client.open(url);
connection.setIfModifiedSince(1393666200000L);
assertEquals("A", readAscii(connection));
RecordedRequest request = server.takeRequest();
@@ -1215,10 +1081,9 @@
.setBody("A"));
server.enqueue(new MockResponse()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
// The first request has no conditions.
RecordedRequest request1 = server.takeRequest();
@@ -1231,54 +1096,23 @@
@Test public void clientSuppliedConditionWithoutCachedResult() throws Exception {
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
String clientIfModifiedSince = formatDate(-24, TimeUnit.HOURS);
connection.addRequestProperty("If-Modified-Since", clientIfModifiedSince);
assertEquals(HttpURLConnection.HTTP_NOT_MODIFIED, connection.getResponseCode());
assertEquals("", readAscii(connection));
}
- @Test public void authorizationRequestHeaderPreventsCaching() throws Exception {
- server.enqueue(
- new MockResponse().addHeader("Last-Modified: " + formatDate(-2, TimeUnit.MINUTES))
- .addHeader("Cache-Control: max-age=60")
- .setBody("A"));
+ @Test public void authorizationRequestFullyCached() throws Exception {
+ server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection = openConnection(url);
+ URLConnection connection = client.open(url);
connection.addRequestProperty("Authorization", "password");
assertEquals("A", readAscii(connection));
- assertEquals("B", readAscii(openConnection(url)));
- }
-
- @Test public void authorizationResponseCachedWithSMaxAge() throws Exception {
- assertAuthorizationRequestFullyCached(
- new MockResponse().addHeader("Cache-Control: s-maxage=60"));
- }
-
- @Test public void authorizationResponseCachedWithPublic() throws Exception {
- assertAuthorizationRequestFullyCached(new MockResponse().addHeader("Cache-Control: public"));
- }
-
- @Test public void authorizationResponseCachedWithMustRevalidate() throws Exception {
- assertAuthorizationRequestFullyCached(
- new MockResponse().addHeader("Cache-Control: must-revalidate"));
- }
-
- public void assertAuthorizationRequestFullyCached(MockResponse response) throws Exception {
- server.enqueue(response.addHeader("Cache-Control: max-age=60").setBody("A"));
- server.enqueue(new MockResponse().setBody("B"));
- server.play();
-
- URL url = server.getUrl("/");
- URLConnection connection = openConnection(url);
- connection.addRequestProperty("Authorization", "password");
- assertEquals("A", readAscii(connection));
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
}
@Test public void contentLocationDoesNotPopulateCache() throws Exception {
@@ -1286,46 +1120,43 @@
.addHeader("Content-Location: /bar")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/foo"))));
- assertEquals("B", readAscii(openConnection(server.getUrl("/bar"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/foo"))));
+ assertEquals("B", readAscii(client.open(server.getUrl("/bar"))));
}
@Test public void useCachesFalseDoesNotWriteToCache() throws Exception {
server.enqueue(
new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.setUseCaches(false);
assertEquals("A", readAscii(connection));
- assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("B", readAscii(client.open(server.getUrl("/"))));
}
@Test public void useCachesFalseDoesNotReadFromCache() throws Exception {
server.enqueue(
new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A").setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.setUseCaches(false);
assertEquals("B", readAscii(connection));
}
@Test public void defaultUseCachesSetsInitialValueOnly() throws Exception {
URL url = new URL("http://localhost/");
- URLConnection c1 = openConnection(url);
- URLConnection c2 = openConnection(url);
+ URLConnection c1 = client.open(url);
+ URLConnection c2 = client.open(url);
assertTrue(c1.getDefaultUseCaches());
c1.setDefaultUseCaches(false);
try {
assertTrue(c1.getUseCaches());
assertTrue(c2.getUseCaches());
- URLConnection c3 = openConnection(url);
+ URLConnection c3 = client.open(url);
assertFalse(c3.getUseCaches());
} finally {
c1.setDefaultUseCaches(true);
@@ -1338,11 +1169,10 @@
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
- assertEquals("A", readAscii(openConnection(server.getUrl("/a"))));
- assertEquals("B", readAscii(openConnection(server.getUrl("/b"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/a"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/a"))));
+ assertEquals("B", readAscii(client.open(server.getUrl("/b"))));
assertEquals(0, server.takeRequest().getSequenceNumber());
assertEquals(1, server.takeRequest().getSequenceNumber());
@@ -1355,14 +1185,13 @@
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
server.enqueue(new MockResponse().setBody("C"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals(1, cache.getRequestCount());
assertEquals(1, cache.getNetworkCount());
assertEquals(0, cache.getHitCount());
- assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("C", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("B", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("C", readAscii(client.open(server.getUrl("/"))));
assertEquals(3, cache.getRequestCount());
assertEquals(3, cache.getNetworkCount());
assertEquals(0, cache.getHitCount());
@@ -1374,14 +1203,13 @@
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals(1, cache.getRequestCount());
assertEquals(1, cache.getNetworkCount());
assertEquals(0, cache.getHitCount());
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals(3, cache.getRequestCount());
assertEquals(3, cache.getNetworkCount());
assertEquals(2, cache.getHitCount());
@@ -1389,14 +1217,13 @@
@Test public void statisticsFullCacheHit() throws Exception {
server.enqueue(new MockResponse().addHeader("Cache-Control: max-age=60").setBody("A"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals(1, cache.getRequestCount());
assertEquals(1, cache.getNetworkCount());
assertEquals(0, cache.getHitCount());
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals(3, cache.getRequestCount());
assertEquals(1, cache.getNetworkCount());
assertEquals(2, cache.getHitCount());
@@ -1407,14 +1234,13 @@
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- HttpURLConnection frConnection = openConnection(url);
+ HttpURLConnection frConnection = client.open(url);
frConnection.addRequestProperty("Accept-Language", "fr-CA");
assertEquals("A", readAscii(frConnection));
- HttpURLConnection enConnection = openConnection(url);
+ HttpURLConnection enConnection = client.open(url);
enConnection.addRequestProperty("Accept-Language", "en-US");
assertEquals("B", readAscii(enConnection));
}
@@ -1424,13 +1250,12 @@
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection1 = openConnection(url);
+ URLConnection connection1 = client.open(url);
connection1.addRequestProperty("Accept-Language", "fr-CA");
assertEquals("A", readAscii(connection1));
- URLConnection connection2 = openConnection(url);
+ URLConnection connection2 = client.open(url);
connection2.addRequestProperty("Accept-Language", "fr-CA");
assertEquals("A", readAscii(connection2));
}
@@ -1440,10 +1265,9 @@
.addHeader("Vary: Foo")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
}
@Test public void varyMatchesAddedRequestHeaderField() throws Exception {
@@ -1451,10 +1275,9 @@
.addHeader("Vary: Foo")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- URLConnection fooConnection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ URLConnection fooConnection = client.open(server.getUrl("/"));
fooConnection.addRequestProperty("Foo", "bar");
assertEquals("B", readAscii(fooConnection));
}
@@ -1464,12 +1287,11 @@
.addHeader("Vary: Foo")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- URLConnection fooConnection = openConnection(server.getUrl("/"));
+ URLConnection fooConnection = client.open(server.getUrl("/"));
fooConnection.addRequestProperty("Foo", "bar");
assertEquals("A", readAscii(fooConnection));
- assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("B", readAscii(client.open(server.getUrl("/"))));
}
@Test public void varyFieldsAreCaseInsensitive() throws Exception {
@@ -1477,13 +1299,12 @@
.addHeader("Vary: ACCEPT-LANGUAGE")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection1 = openConnection(url);
+ URLConnection connection1 = client.open(url);
connection1.addRequestProperty("Accept-Language", "fr-CA");
assertEquals("A", readAscii(connection1));
- URLConnection connection2 = openConnection(url);
+ URLConnection connection2 = client.open(url);
connection2.addRequestProperty("accept-language", "fr-CA");
assertEquals("A", readAscii(connection2));
}
@@ -1494,15 +1315,14 @@
.addHeader("Vary: Accept-Encoding")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection1 = openConnection(url);
+ URLConnection connection1 = client.open(url);
connection1.addRequestProperty("Accept-Language", "fr-CA");
connection1.addRequestProperty("Accept-Charset", "UTF-8");
connection1.addRequestProperty("Accept-Encoding", "identity");
assertEquals("A", readAscii(connection1));
- URLConnection connection2 = openConnection(url);
+ URLConnection connection2 = client.open(url);
connection2.addRequestProperty("Accept-Language", "fr-CA");
connection2.addRequestProperty("Accept-Charset", "UTF-8");
connection2.addRequestProperty("Accept-Encoding", "identity");
@@ -1515,15 +1335,14 @@
.addHeader("Vary: Accept-Encoding")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection frConnection = openConnection(url);
+ URLConnection frConnection = client.open(url);
frConnection.addRequestProperty("Accept-Language", "fr-CA");
frConnection.addRequestProperty("Accept-Charset", "UTF-8");
frConnection.addRequestProperty("Accept-Encoding", "identity");
assertEquals("A", readAscii(frConnection));
- URLConnection enConnection = openConnection(url);
+ URLConnection enConnection = client.open(url);
enConnection.addRequestProperty("Accept-Language", "en-CA");
enConnection.addRequestProperty("Accept-Charset", "UTF-8");
enConnection.addRequestProperty("Accept-Encoding", "identity");
@@ -1535,15 +1354,14 @@
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection1 = openConnection(url);
+ URLConnection connection1 = client.open(url);
connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
connection1.addRequestProperty("Accept-Language", "en-US");
assertEquals("A", readAscii(connection1));
- URLConnection connection2 = openConnection(url);
+ URLConnection connection2 = client.open(url);
connection2.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
connection2.addRequestProperty("Accept-Language", "en-US");
assertEquals("A", readAscii(connection2));
@@ -1554,15 +1372,14 @@
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- URLConnection connection1 = openConnection(url);
+ URLConnection connection1 = client.open(url);
connection1.addRequestProperty("Accept-Language", "fr-CA, fr-FR");
connection1.addRequestProperty("Accept-Language", "en-US");
assertEquals("A", readAscii(connection1));
- URLConnection connection2 = openConnection(url);
+ URLConnection connection2 = client.open(url);
connection2.addRequestProperty("Accept-Language", "fr-CA");
connection2.addRequestProperty("Accept-Language", "en-US");
assertEquals("B", readAscii(connection2));
@@ -1573,10 +1390,9 @@
.addHeader("Vary: *")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- assertEquals("B", readAscii(openConnection(server.getUrl("/"))));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ assertEquals("B", readAscii(client.open(server.getUrl("/"))));
}
@Test public void varyAndHttps() throws Exception {
@@ -1585,7 +1401,6 @@
.addHeader("Vary: Accept-Language")
.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
HttpsURLConnection connection1 = (HttpsURLConnection) client.open(url);
@@ -1610,12 +1425,11 @@
server.enqueue(new MockResponse().addHeader(
"Set-Cookie: a=SECOND; domain=" + server.getCookieDomain() + ";")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
assertCookies(url, "a=FIRST");
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
assertCookies(url, "a=SECOND");
}
@@ -1626,13 +1440,12 @@
.setBody("A"));
server.enqueue(new MockResponse().addHeader("Allow: GET, HEAD, PUT")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- URLConnection connection1 = openConnection(server.getUrl("/"));
+ URLConnection connection1 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
assertEquals("GET, HEAD", connection1.getHeaderField("Allow"));
- URLConnection connection2 = openConnection(server.getUrl("/"));
+ URLConnection connection2 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection2));
assertEquals("GET, HEAD, PUT", connection2.getHeaderField("Allow"));
}
@@ -1644,13 +1457,12 @@
.setBody("A"));
server.enqueue(new MockResponse().addHeader("Transfer-Encoding: none")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- URLConnection connection1 = openConnection(server.getUrl("/"));
+ URLConnection connection1 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
assertEquals("identity", connection1.getHeaderField("Transfer-Encoding"));
- URLConnection connection2 = openConnection(server.getUrl("/"));
+ URLConnection connection2 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection2));
assertEquals("identity", connection2.getHeaderField("Transfer-Encoding"));
}
@@ -1661,13 +1473,12 @@
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- URLConnection connection1 = openConnection(server.getUrl("/"));
+ URLConnection connection1 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
assertEquals("199 test danger", connection1.getHeaderField("Warning"));
- URLConnection connection2 = openConnection(server.getUrl("/"));
+ URLConnection connection2 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection2));
assertEquals(null, connection2.getHeaderField("Warning"));
}
@@ -1678,19 +1489,18 @@
.addHeader("Cache-Control: max-age=0")
.setBody("A"));
server.enqueue(new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
- URLConnection connection1 = openConnection(server.getUrl("/"));
+ URLConnection connection1 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection1));
assertEquals("299 test danger", connection1.getHeaderField("Warning"));
- URLConnection connection2 = openConnection(server.getUrl("/"));
+ URLConnection connection2 = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection2));
assertEquals("299 test danger", connection2.getHeaderField("Warning"));
}
public void assertCookies(URL url, String... expectedCookies) throws Exception {
- List<String> actualCookies = new ArrayList<String>();
+ List<String> actualCookies = new ArrayList<>();
for (HttpCookie cookie : cookieManager.getCookieStore().get(url.toURI())) {
actualCookies.add(cookie.toString());
}
@@ -1699,9 +1509,9 @@
@Test public void cachePlusRange() throws Exception {
assertNotCached(new MockResponse().setResponseCode(HttpURLConnection.HTTP_PARTIAL)
- .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
- .addHeader("Content-Range: bytes 100-100/200")
- .addHeader("Cache-Control: max-age=60"));
+ .addHeader("Date: " + formatDate(0, TimeUnit.HOURS))
+ .addHeader("Content-Range: bytes 100-100/200")
+ .addHeader("Cache-Control: max-age=60"));
}
@Test public void conditionalHitUpdatesCache() throws Exception {
@@ -1712,21 +1522,20 @@
.addHeader("Allow: GET, HEAD")
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
// cache miss; seed the cache
- HttpURLConnection connection1 = openConnection(server.getUrl("/a"));
+ HttpURLConnection connection1 = client.open(server.getUrl("/a"));
assertEquals("A", readAscii(connection1));
assertEquals(null, connection1.getHeaderField("Allow"));
// conditional cache hit; update the cache
- HttpURLConnection connection2 = openConnection(server.getUrl("/a"));
+ HttpURLConnection connection2 = client.open(server.getUrl("/a"));
assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode());
assertEquals("A", readAscii(connection2));
assertEquals("GET, HEAD", connection2.getHeaderField("Allow"));
// full cache hit
- HttpURLConnection connection3 = openConnection(server.getUrl("/a"));
+ HttpURLConnection connection3 = client.open(server.getUrl("/a"));
assertEquals("A", readAscii(connection3));
assertEquals("GET, HEAD", connection3.getHeaderField("Allow"));
@@ -1737,15 +1546,11 @@
server.enqueue(new MockResponse().setBody("A")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- URLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ URLConnection connection = client.open(server.getUrl("/"));
connection.addRequestProperty("Cache-Control", "only-if-cached");
assertEquals("A", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.CACHE + " 200", source);
}
@Test public void responseSourceHeaderConditionalCacheFetched() throws IOException {
@@ -1755,14 +1560,10 @@
server.enqueue(new MockResponse().setBody("B")
.addHeader("Cache-Control: max-age=30")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("B", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.CONDITIONAL_CACHE + " 200", source);
}
@Test public void responseSourceHeaderConditionalCacheNotFetched() throws IOException {
@@ -1770,33 +1571,25 @@
.addHeader("Cache-Control: max-age=0")
.addHeader("Date: " + formatDate(0, TimeUnit.MINUTES)));
server.enqueue(new MockResponse().setResponseCode(304));
- server.play();
- assertEquals("A", readAscii(openConnection(server.getUrl("/"))));
- HttpURLConnection connection = openConnection(server.getUrl("/"));
+ assertEquals("A", readAscii(client.open(server.getUrl("/"))));
+ HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.CONDITIONAL_CACHE + " 304", source);
}
@Test public void responseSourceHeaderFetched() throws IOException {
server.enqueue(new MockResponse().setBody("A"));
- server.play();
- URLConnection connection = openConnection(server.getUrl("/"));
+ URLConnection connection = client.open(server.getUrl("/"));
assertEquals("A", readAscii(connection));
-
- String source = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- assertEquals(ResponseSource.NETWORK + " 200", source);
}
@Test public void emptyResponseHeaderNameFromCacheIsLenient() throws Exception {
- server.enqueue(new MockResponse()
- .addHeader("Cache-Control: max-age=120")
- .addHeader(": A")
- .setBody("body"));
- server.play();
+ Headers.Builder headers = new Headers.Builder()
+ .add("Cache-Control: max-age=120");
+ Internal.instance.addLenient(headers, ": A");
+ server.enqueue(new MockResponse().setHeaders(headers.build()).setBody("body"));
+
HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("A", connection.getHeaderField(""));
}
@@ -1814,10 +1607,9 @@
server.enqueue(new MockResponse()
.clearHeaders()
.setResponseCode(HttpURLConnection.HTTP_NOT_MODIFIED));
- server.play();
URL url = server.getUrl("/");
- String urlKey = Util.hash(url.toString());
+ String urlKey = Util.md5Hex(url.toString());
String entryMetadata = ""
+ "" + url + "\n"
+ "GET\n"
@@ -1853,8 +1645,8 @@
writeFile(cache.getDirectory(), urlKey + ".0", entryMetadata);
writeFile(cache.getDirectory(), urlKey + ".1", entryBody);
writeFile(cache.getDirectory(), "journal", journalBody);
- cache = new HttpResponseCache(cache.getDirectory(), Integer.MAX_VALUE);
- client.setOkResponseCache(cache);
+ cache = new Cache(cache.getDirectory(), Integer.MAX_VALUE);
+ client.client().setCache(cache);
HttpURLConnection connection = client.open(url);
assertEquals(entryBody, readAscii(connection));
@@ -1862,66 +1654,10 @@
assertEquals("foo", connection.getHeaderField("etag"));
}
- // Older versions of OkHttp use ResponseCache.get() and ResponseCache.put(). For compatibility
- // with Android apps when the Android-bundled and and an older app-bundled OkHttp library are in
- // use at the same time the HttpResponseCache must behave as it always used to. That's not the
- // same as a fully API-compliant {@link ResponseCache}: That means that the cache
- // doesn't throw an exception from get() or put() and also does not cache requests/responses from
- // anything other than the variant of OkHttp that it comes with. It does still return values from
- // get() and it is not expected to implement any cache-control logic.
- @Test public void testHttpResponseCacheBackwardsCompatible() throws Exception {
- assertSame(cache, ResponseCache.getDefault());
- assertEquals(0, cache.getRequestCount());
-
- String body = "Body";
- server.enqueue(new MockResponse()
- .addHeader("Last-Modified: " + formatDate(-1, TimeUnit.HOURS))
- .addHeader("Expires: " + formatDate(1, TimeUnit.HOURS))
- .setBody(body));
- server.play();
-
- URL url = server.getUrl("/");
-
- // Here we use a HttpURLConnection from URL to represent a non-OkHttp HttpURLConnection. In
- // Android this would be com.android.okhttp.internal.http.HttpURLConnectionImpl. In tests this
- // is some other implementation.
- HttpURLConnection javaConnection = (HttpURLConnection) url.openConnection();
- assertFalse("This test relies on url.openConnection() not returning an OkHttp connection",
- javaConnection instanceof HttpURLConnectionImpl);
- javaConnection.disconnect();
-
- // This should simply be discarded. It doesn't matter the connection is not useful.
- cache.put(url.toURI(), javaConnection);
-
- // Confirm the initial cache state.
- assertNull(cache.get(url.toURI(), "GET", new HashMap<String, List<String>>()));
-
- // Now cache a response
- HttpURLConnection okHttpConnection = openConnection(url);
- assertEquals(body, readAscii(okHttpConnection));
- okHttpConnection.disconnect();
-
- assertEquals(1, server.getRequestCount());
- assertEquals(0, cache.getHitCount());
-
- // OkHttp should now find the result cached.
- HttpURLConnection okHttpConnection2 = openConnection(url);
- assertEquals(body, readAscii(okHttpConnection2));
- okHttpConnection2.disconnect();
-
- assertEquals(1, server.getRequestCount());
- assertEquals(1, cache.getHitCount());
-
- // Confirm the unfortunate get() behavior.
- assertNotNull(cache.get(url.toURI(), "GET", new HashMap<String, List<String>>()));
- // Only OkHttp makes the necessary callbacks to increment the cache stats.
- assertEquals(1, cache.getHitCount());
- }
-
private void writeFile(File directory, String file, String content) throws IOException {
- OutputStream out = new FileOutputStream(new File(directory, file));
- out.write(content.getBytes(Util.UTF_8));
- out.close();
+ BufferedSink sink = Okio.buffer(Okio.sink(new File(directory, file)));
+ sink.writeUtf8(content);
+ sink.close();
}
/**
@@ -1952,11 +1688,10 @@
private void assertNotCached(MockResponse response) throws Exception {
server.enqueue(response.setBody("A"));
server.enqueue(new MockResponse().setBody("B"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- assertEquals("B", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
+ assertEquals("B", readAscii(client.open(url)));
}
/** @return the request with the conditional get headers. */
@@ -1969,24 +1704,22 @@
server.enqueue(response.setBody("B").setStatus("HTTP/1.1 200 B-OK"));
server.enqueue(new MockResponse().setStatus("HTTP/1.1 200 C-OK").setBody("C"));
- server.play();
-
URL valid = server.getUrl("/valid");
- HttpURLConnection connection1 = openConnection(valid);
+ HttpURLConnection connection1 = client.open(valid);
assertEquals("A", readAscii(connection1));
assertEquals(HttpURLConnection.HTTP_OK, connection1.getResponseCode());
assertEquals("A-OK", connection1.getResponseMessage());
- HttpURLConnection connection2 = openConnection(valid);
+ HttpURLConnection connection2 = client.open(valid);
assertEquals("A", readAscii(connection2));
assertEquals(HttpURLConnection.HTTP_OK, connection2.getResponseCode());
assertEquals("A-OK", connection2.getResponseMessage());
URL invalid = server.getUrl("/invalid");
- HttpURLConnection connection3 = openConnection(invalid);
+ HttpURLConnection connection3 = client.open(invalid);
assertEquals("B", readAscii(connection3));
assertEquals(HttpURLConnection.HTTP_OK, connection3.getResponseCode());
assertEquals("B-OK", connection3.getResponseMessage());
- HttpURLConnection connection4 = openConnection(invalid);
+ HttpURLConnection connection4 = client.open(invalid);
assertEquals("C", readAscii(connection4));
assertEquals(HttpURLConnection.HTTP_OK, connection4.getResponseCode());
assertEquals("C-OK", connection4.getResponseMessage());
@@ -1998,11 +1731,10 @@
private void assertFullyCached(MockResponse response) throws Exception {
server.enqueue(response.setBody("A"));
server.enqueue(response.setBody("B"));
- server.play();
URL url = server.getUrl("/");
- assertEquals("A", readAscii(openConnection(url)));
- assertEquals("A", readAscii(openConnection(url)));
+ assertEquals("A", readAscii(client.open(url)));
+ assertEquals("A", readAscii(client.open(url)));
}
/**
@@ -2012,10 +1744,11 @@
*/
private MockResponse truncateViolently(MockResponse response, int numBytesToKeep) {
response.setSocketPolicy(DISCONNECT_AT_END);
- List<String> headers = new ArrayList<String>(response.getHeaders());
- response.setBody(Arrays.copyOfRange(response.getBody(), 0, numBytesToKeep));
- response.getHeaders().clear();
- response.getHeaders().addAll(headers);
+ Headers headers = response.getHeaders();
+ Buffer truncatedBody = new Buffer();
+ truncatedBody.write(response.getBody(), numBytesToKeep);
+ response.setBody(truncatedBody);
+ response.setHeaders(headers);
return response;
}
@@ -2059,39 +1792,32 @@
}
assertEquals(504, connection.getResponseCode());
assertEquals(-1, connection.getErrorStream().read());
- assertEquals(ResponseSource.NONE + " 504",
- connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
}
enum TransferKind {
CHUNKED() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize)
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize)
throws IOException {
response.setChunkedBody(content, chunkSize);
}
},
FIXED_LENGTH() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
}
},
END_OF_STREAM() {
- @Override void setBody(MockResponse response, byte[] content, int chunkSize) {
+ @Override void setBody(MockResponse response, Buffer content, int chunkSize) {
response.setBody(content);
response.setSocketPolicy(DISCONNECT_AT_END);
- for (Iterator<String> h = response.getHeaders().iterator(); h.hasNext(); ) {
- if (h.next().startsWith("Content-Length:")) {
- h.remove();
- break;
- }
- }
+ response.removeHeader("Content-Length");
}
};
- abstract void setBody(MockResponse response, byte[] content, int chunkSize) throws IOException;
+ abstract void setBody(MockResponse response, Buffer content, int chunkSize) throws IOException;
void setBody(MockResponse response, String content, int chunkSize) throws IOException {
- setBody(response, content.getBytes("UTF-8"), chunkSize);
+ setBody(response, new Buffer().writeUtf8(content), chunkSize);
}
}
@@ -2100,34 +1826,11 @@
}
/** Returns a gzipped copy of {@code bytes}. */
- public byte[] gzip(byte[] bytes) throws IOException {
- ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
- OutputStream gzippedOut = new GZIPOutputStream(bytesOut);
- gzippedOut.write(bytes);
- gzippedOut.close();
- return bytesOut.toByteArray();
- }
-
- static abstract class AbstractOkResponseCache implements OkResponseCache {
- @Override public Response get(Request request) throws IOException {
- return null;
- }
-
- @Override public CacheRequest put(Response response) throws IOException {
- return null;
- }
-
- @Override public boolean maybeRemove(Request request) throws IOException {
- return false;
- }
-
- @Override public void update(Response cached, Response network) throws IOException {
- }
-
- @Override public void trackConditionalCacheHit() {
- }
-
- @Override public void trackResponse(ResponseSource source) {
- }
+ public Buffer gzip(String data) throws IOException {
+ Buffer result = new Buffer();
+ BufferedSink sink = Okio.buffer(new GzipSink(result));
+ sink.writeUtf8(data);
+ sink.close();
+ return result;
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/URLEncodingTest.java
similarity index 83%
rename from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
rename to okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/URLEncodingTest.java
index 6ca3756..5a4ed10 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLEncodingTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/internal/huc/URLEncodingTest.java
@@ -14,18 +14,23 @@
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.huc;
import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.OkUrlFactory;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.InternalCache;
+import com.squareup.okhttp.internal.http.CacheRequest;
+import com.squareup.okhttp.internal.http.CacheStrategy;
+
import java.io.IOException;
-import java.net.CacheRequest;
import java.net.CacheResponse;
import java.net.HttpURLConnection;
-import java.net.ResponseCache;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
-import java.net.URLConnection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@@ -121,23 +126,44 @@
}
private URI backdoorUrlToUri(URL url) throws Exception {
- final AtomicReference<URI> uriReference = new AtomicReference<URI>();
+ final AtomicReference<URI> uriReference = new AtomicReference<>();
OkHttpClient client = new OkHttpClient();
- client.setResponseCache(new ResponseCache() {
- @Override public CacheRequest put(URI uri, URLConnection connection) throws IOException {
+ Internal.instance.setCache(client, new InternalCache() {
+ @Override
+ public Response get(Request request) throws IOException {
+ uriReference.set(request.uri());
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public CacheRequest put(Response response) throws IOException {
return null;
}
- @Override public CacheResponse get(URI uri, String requestMethod,
- Map<String, List<String>> requestHeaders) throws IOException {
- uriReference.set(uri);
- throw new UnsupportedOperationException();
+ @Override
+ public void remove(Request request) throws IOException {
+
+ }
+
+ @Override
+ public void update(Response cached, Response network) throws IOException {
+
+ }
+
+ @Override
+ public void trackConditionalCacheHit() {
+
+ }
+
+ @Override
+ public void trackResponse(CacheStrategy cacheStrategy) {
+
}
});
try {
- HttpURLConnection connection = client.open(url);
+ HttpURLConnection connection = new OkUrlFactory(client).open(url);
connection.getResponseCode();
} catch (Exception expected) {
if (expected.getCause() instanceof URISyntaxException) {
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index c7e3ec4..b723221 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>okhttp</artifactId>
@@ -16,17 +16,31 @@
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
- <version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>templating-maven-plugin</artifactId>
+ <version>1.0-alpha-3</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>filter-sources</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<configuration>
<excludePackageNames>com.squareup.okhttp.internal:com.squareup.okhttp.internal.*</excludePackageNames>
+ <links>
+ <link>http://square.github.io/okio/</link>
+ </links>
</configuration>
</plugin>
</plugins>
diff --git a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java b/okhttp/src/main/java-templates/com/squareup/okhttp/internal/Version.java
similarity index 76%
rename from okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
rename to okhttp/src/main/java-templates/com/squareup/okhttp/internal/Version.java
index ac3de72..59fece9 100644
--- a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
+++ b/okhttp/src/main/java-templates/com/squareup/okhttp/internal/Version.java
@@ -13,10 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package okio;
+package com.squareup.okhttp.internal;
-public final class OkBufferReadUtf8LineTest extends ReadUtf8LineTest {
- @Override protected BufferedSource newSource(String s) {
- return new OkBuffer().writeUtf8(s);
+public final class Version {
+ public static String userAgent() {
+ return "okhttp/${project.version}";
+ }
+
+ private Version() {
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Address.java b/okhttp/src/main/java/com/squareup/okhttp/Address.java
index 8f80cf4..38768a4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Address.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Address.java
@@ -17,7 +17,7 @@
import com.squareup.okhttp.internal.Util;
import java.net.Proxy;
-import java.net.UnknownHostException;
+import java.net.ProxySelector;
import java.util.List;
import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
@@ -28,7 +28,7 @@
/**
* A specification for a connection to an origin server. For simple connections,
* this is the server's hostname and port. If an explicit proxy is requested (or
- * {@link Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
+ * {@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.
*
@@ -42,25 +42,32 @@
final SocketFactory socketFactory;
final SSLSocketFactory sslSocketFactory;
final HostnameVerifier hostnameVerifier;
- final OkAuthenticator authenticator;
+ final CertificatePinner certificatePinner;
+ final Authenticator authenticator;
final List<Protocol> protocols;
+ final List<ConnectionSpec> connectionSpecs;
+ final ProxySelector proxySelector;
public Address(String uriHost, int uriPort, SocketFactory socketFactory,
SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,
- OkAuthenticator authenticator, Proxy proxy, List<Protocol> protocols)
- throws UnknownHostException {
+ 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.socketFactory = socketFactory;
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. */
@@ -97,31 +104,41 @@
return hostnameVerifier;
}
-
/**
* Returns the client's authenticator. This method never returns null.
*/
- public OkAuthenticator getAuthenticator() {
+ public Authenticator getAuthenticator() {
return authenticator;
}
/**
* Returns the protocols the client supports. This method always returns a
- * non-null list that contains minimally
- * {@link Protocol#HTTP_11}.
+ * non-null list that contains minimally {@link Protocol#HTTP_1_1}.
*/
public List<Protocol> getProtocols() {
return protocols;
}
+ public List<ConnectionSpec> getConnectionSpecs() {
+ return connectionSpecs;
+ }
+
/**
* Returns this address's explicitly-specified HTTP proxy, or null to
- * delegate to the HTTP client's proxy selector.
+ * 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.
+ */
+ public ProxySelector getProxySelector() {
+ return proxySelector;
+ }
+
@Override public boolean equals(Object other) {
if (other instanceof Address) {
Address that = (Address) other;
@@ -130,21 +147,27 @@
&& this.uriPort == that.uriPort
&& 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.protocols, that.protocols)
+ && equal(this.connectionSpecs, that.connectionSpecs)
+ && equal(this.proxySelector, that.proxySelector);
}
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 + (authenticator != null ? authenticator.hashCode() : 0);
- result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+ result = 31 * result + (certificatePinner != null ? certificatePinner.hashCode() : 0);
+ result = 31 * result + authenticator.hashCode();
result = 31 * result + protocols.hashCode();
+ result = 31 * result + connectionSpecs.hashCode();
+ result = 31 * result + proxySelector.hashCode();
return result;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Authenticator.java b/okhttp/src/main/java/com/squareup/okhttp/Authenticator.java
new file mode 100644
index 0000000..cb66dc6
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Authenticator.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import java.io.IOException;
+import java.net.Proxy;
+
+/**
+ * Responds to authentication challenges from the remote web or proxy server.
+ */
+public interface Authenticator {
+ /**
+ * Returns a request that includes a credential to satisfy an authentication
+ * challenge in {@code response}. Returns null if the challenge cannot be
+ * satisfied. This method is called in response to an HTTP 401 unauthorized
+ * status code sent by the origin server.
+ *
+ * <p>Typical implementations will look up a credential and create a request
+ * derived from the initial request by setting the "Authorization" header.
+ * <pre> {@code
+ *
+ * String credential = Credentials.basic(...)
+ * return response.request().newBuilder()
+ * .header("Authorization", credential)
+ * .build();
+ * }</pre>
+ */
+ Request authenticate(Proxy proxy, Response response) throws IOException;
+
+ /**
+ * Returns a request that includes a credential to satisfy an authentication
+ * challenge made by {@code response}. Returns null if the challenge cannot be
+ * satisfied. This method is called in response to an HTTP 407 unauthorized
+ * status code sent by the proxy server.
+ *
+ * <p>Typical implementations will look up a credential and create a request
+ * derived from the initial request by setting the "Proxy-Authorization"
+ * header. <pre> {@code
+ *
+ * String credential = Credentials.basic(...)
+ * return response.request().newBuilder()
+ * .header("Proxy-Authorization", credential)
+ * .build();
+ * }</pre>
+ */
+ Request authenticateProxy(Proxy proxy, Response response) throws IOException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/Cache.java
similarity index 64%
rename from okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
rename to okhttp/src/main/java/com/squareup/okhttp/Cache.java
index d016877..2b98355 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpResponseCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Cache.java
@@ -17,64 +17,46 @@
package com.squareup.okhttp;
import com.squareup.okhttp.internal.DiskLruCache;
+import com.squareup.okhttp.internal.InternalCache;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.CacheRequest;
+import com.squareup.okhttp.internal.http.CacheStrategy;
import com.squareup.okhttp.internal.http.HttpMethod;
-import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
-import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
-import com.squareup.okhttp.internal.http.JavaApiConverter;
-import java.io.BufferedWriter;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.StatusLine;
import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.FilterInputStream;
-import java.io.FilterOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-import java.net.CacheRequest;
-import java.net.CacheResponse;
-import java.net.ResponseCache;
-import java.net.URI;
-import java.net.URLConnection;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Iterator;
import java.util.List;
-import java.util.Map;
+import java.util.NoSuchElementException;
+import okio.BufferedSink;
import okio.BufferedSource;
import okio.ByteString;
+import okio.ForwardingSink;
+import okio.ForwardingSource;
import okio.Okio;
-
-import static com.squareup.okhttp.internal.Util.UTF_8;
+import okio.Sink;
+import okio.Source;
/**
* Caches HTTP and HTTPS responses to the filesystem so they may be reused,
* saving time and bandwidth.
*
- * <p>This cache extends {@link ResponseCache} but is only intended for use
- * with OkHttp and is not a general-purpose implementation: The
- * {@link ResponseCache} API requires that the subclass handles cache-control
- * logic as well as storage. In OkHttp the {@link HttpResponseCache} only
- * handles cursory cache-control logic.
- *
- * <p>To maintain support for previous releases the {@link HttpResponseCache}
- * will disregard any {@link #put(java.net.URI, java.net.URLConnection)}
- * calls with a URLConnection that is not from OkHttp. It will, however,
- * return cached data for any calls to {@link #get(java.net.URI, String,
- * java.util.Map)}.
- *
* <h3>Cache Optimization</h3>
* To measure cache effectiveness, this class tracks three statistics:
* <ul>
- * <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
- * of HTTP requests issued since this cache was created.
- * <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
+ * <li><strong>{@linkplain #getRequestCount() Request Count:}</strong> the
+ * number of HTTP requests issued since this cache was created.
+ * <li><strong>{@linkplain #getNetworkCount() Network Count:}</strong> the
* number of those requests that required network use.
- * <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
+ * <li><strong>{@linkplain #getHitCount() Hit Count:}</strong> the number of
* those requests whose responses were served by the cache.
* </ul>
* Sometimes a request will result in a conditional cache hit. If the cache
@@ -120,13 +102,33 @@
* connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
* }</pre>
*/
-public final class HttpResponseCache extends ResponseCache implements OkResponseCache {
- // TODO: add APIs to iterate the cache?
+public final class Cache {
private static final int VERSION = 201105;
private static final int ENTRY_METADATA = 0;
private static final int ENTRY_BODY = 1;
private static final int ENTRY_COUNT = 2;
+ final InternalCache internalCache = new InternalCache() {
+ @Override public Response get(Request request) throws IOException {
+ return Cache.this.get(request);
+ }
+ @Override public CacheRequest put(Response response) throws IOException {
+ return Cache.this.put(response);
+ }
+ @Override public void remove(Request request) throws IOException {
+ Cache.this.remove(request);
+ }
+ @Override public void update(Response cached, Response network) throws IOException {
+ Cache.this.update(cached, network);
+ }
+ @Override public void trackConditionalCacheHit() {
+ Cache.this.trackConditionalCacheHit();
+ }
+ @Override public void trackResponse(CacheStrategy cacheStrategy) {
+ Cache.this.trackResponse(cacheStrategy);
+ }
+ };
+
private final DiskLruCache cache;
/* read and write statistics, all guarded by 'this' */
@@ -136,27 +138,15 @@
private int hitCount;
private int requestCount;
- public HttpResponseCache(File directory, long maxSize) throws IOException {
- cache = DiskLruCache.open(directory, VERSION, ENTRY_COUNT, maxSize);
+ public Cache(File directory, long maxSize) {
+ cache = DiskLruCache.create(directory, VERSION, ENTRY_COUNT, maxSize);
}
- @Override public CacheResponse get(
- URI uri, String requestMethod, Map<String, List<String>> requestHeaders)
- throws IOException {
-
- Request request = JavaApiConverter.createOkRequest(uri, requestMethod, requestHeaders);
- Response response = get(request);
- if (response == null) {
- return null;
- }
- return JavaApiConverter.createJavaCacheResponse(response);
+ private static String urlToKey(Request request) {
+ return Util.md5Hex(request.urlString());
}
- private static String urlToKey(Request requst) {
- return Util.hash(requst.urlString());
- }
-
- @Override public Response get(Request request) {
+ Response get(Request request) {
String key = urlToKey(request);
DiskLruCache.Snapshot snapshot;
Entry entry;
@@ -165,12 +155,18 @@
if (snapshot == null) {
return null;
}
- entry = new Entry(snapshot.getInputStream(ENTRY_METADATA));
} catch (IOException e) {
// Give up because the cache cannot be read.
return null;
}
+ try {
+ entry = new Entry(snapshot.getSource(ENTRY_METADATA));
+ } catch (IOException e) {
+ Util.closeQuietly(snapshot);
+ return null;
+ }
+
Response response = entry.response(request, snapshot);
if (!entry.matches(request, response)) {
@@ -181,22 +177,15 @@
return response;
}
- @Override public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
- if (!isCacheableConnection(urlConnection)) {
- return null;
- }
- return put(JavaApiConverter.createOkResponse(uri, urlConnection));
- }
-
- private static boolean isCacheableConnection(URLConnection httpConnection) {
- return (httpConnection instanceof HttpURLConnectionImpl)
- || (httpConnection instanceof HttpsURLConnectionImpl);
- }
-
- @Override public CacheRequest put(Response response) throws IOException {
+ private CacheRequest put(Response response) throws IOException {
String requestMethod = response.request().method();
- if (maybeRemove(response.request())) {
+ if (HttpMethod.invalidatesCache(response.request().method())) {
+ try {
+ remove(response.request());
+ } catch (IOException ignored) {
+ // The cache cannot be written.
+ }
return null;
}
if (!requestMethod.equals("GET")) {
@@ -206,7 +195,7 @@
return null;
}
- if (response.hasVaryAll()) {
+ if (OkHeaders.hasVaryAll(response)) {
return null;
}
@@ -225,19 +214,11 @@
}
}
- @Override public boolean maybeRemove(Request request) {
- if (HttpMethod.invalidatesCache(request.method())) {
- try {
- cache.remove(urlToKey(request));
- } catch (IOException ignored) {
- // The cache cannot be written.
- }
- return true;
- }
- return false;
+ private void remove(Request request) throws IOException {
+ cache.remove(urlToKey(request));
}
- @Override public void update(Response cached, Response network) {
+ private void update(Response cached, Response network) {
Entry entry = new Entry(network);
DiskLruCache.Snapshot snapshot = ((CacheResponseBody) cached.body()).snapshot;
DiskLruCache.Editor editor = null;
@@ -271,6 +252,66 @@
cache.delete();
}
+ /**
+ * Deletes all values stored in the cache. In-flight writes to the cache will
+ * complete normally, but the corresponding responses will not be stored.
+ */
+ public void evictAll() throws IOException {
+ cache.evictAll();
+ }
+
+ /**
+ * Returns an iterator over the URLs in this cache. This iterator doesn't throw {@code
+ * ConcurrentModificationException}, but if new responses are added while iterating, their URLs
+ * will not be returned. If existing responses are evicted during iteration, they will be absent
+ * (unless they were already returned).
+ *
+ * <p>The iterator supports {@linkplain Iterator#remove}. Removing a URL from the iterator evicts
+ * the corresponding response from the cache. Use this to evict selected responses.
+ */
+ public Iterator<String> urls() throws IOException {
+ return new Iterator<String>() {
+ final Iterator<DiskLruCache.Snapshot> delegate = cache.snapshots();
+
+ String nextUrl;
+ boolean canRemove;
+
+ @Override public boolean hasNext() {
+ if (nextUrl != null) return true;
+
+ canRemove = false; // Prevent delegate.remove() on the wrong item!
+ while (delegate.hasNext()) {
+ DiskLruCache.Snapshot snapshot = delegate.next();
+ try {
+ BufferedSource metadata = Okio.buffer(snapshot.getSource(ENTRY_METADATA));
+ nextUrl = metadata.readUtf8LineStrict();
+ return true;
+ } catch (IOException ignored) {
+ // We couldn't read the metadata for this snapshot; possibly because the host filesystem
+ // has disappeared! Skip it.
+ } finally {
+ snapshot.close();
+ }
+ }
+
+ return false;
+ }
+
+ @Override public String next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ String result = nextUrl;
+ nextUrl = null;
+ canRemove = true;
+ return result;
+ }
+
+ @Override public void remove() {
+ if (!canRemove) throw new IllegalStateException("remove() before next()");
+ delegate.remove();
+ }
+ };
+ }
+
public synchronized int getWriteAbortCount() {
return writeAbortCount;
}
@@ -279,7 +320,7 @@
return writeSuccessCount;
}
- public long getSize() {
+ public long getSize() throws IOException {
return cache.size();
}
@@ -303,21 +344,20 @@
return cache.isClosed();
}
- @Override public synchronized void trackResponse(ResponseSource source) {
+ private synchronized void trackResponse(CacheStrategy cacheStrategy) {
requestCount++;
- switch (source) {
- case CACHE:
- hitCount++;
- break;
- case CONDITIONAL_CACHE:
- case NETWORK:
- networkCount++;
- break;
+ if (cacheStrategy.networkRequest != null) {
+ // If this is a conditional request, we'll increment hitCount if/when it hits.
+ networkCount++;
+
+ } else if (cacheStrategy.cacheResponse != null) {
+ // This response uses the cache and not the network. That's a cache hit.
+ hitCount++;
}
}
- @Override public synchronized void trackConditionalCacheHit() {
+ private synchronized void trackConditionalCacheHit() {
hitCount++;
}
@@ -333,18 +373,18 @@
return requestCount;
}
- private final class CacheRequestImpl extends CacheRequest {
+ private final class CacheRequestImpl implements CacheRequest {
private final DiskLruCache.Editor editor;
- private OutputStream cacheOut;
+ private Sink cacheOut;
private boolean done;
- private OutputStream body;
+ private Sink body;
public CacheRequestImpl(final DiskLruCache.Editor editor) throws IOException {
this.editor = editor;
- this.cacheOut = editor.newOutputStream(ENTRY_BODY);
- this.body = new FilterOutputStream(cacheOut) {
+ this.cacheOut = editor.newSink(ENTRY_BODY);
+ this.body = new ForwardingSink(cacheOut) {
@Override public void close() throws IOException {
- synchronized (HttpResponseCache.this) {
+ synchronized (Cache.this) {
if (done) {
return;
}
@@ -354,17 +394,11 @@
super.close();
editor.commit();
}
-
- @Override public void write(byte[] buffer, int offset, int length) throws IOException {
- // Since we don't override "write(int oneByte)", we can write directly to "out"
- // and avoid the inefficient implementation from the FilterOutputStream.
- out.write(buffer, offset, length);
- }
};
}
@Override public void abort() {
- synchronized (HttpResponseCache.this) {
+ synchronized (Cache.this) {
if (done) {
return;
}
@@ -378,7 +412,7 @@
}
}
- @Override public OutputStream getBody() throws IOException {
+ @Override public Sink body() {
return body;
}
}
@@ -387,7 +421,9 @@
private final String url;
private final Headers varyHeaders;
private final String requestMethod;
- private final String statusLine;
+ private final Protocol protocol;
+ private final int code;
+ private final String message;
private final Headers responseHeaders;
private final Handshake handshake;
@@ -440,23 +476,26 @@
* certificates are also base64-encoded and appear each on their own
* line. A length of -1 is used to encode a null array.
*/
- public Entry(InputStream in) throws IOException {
+ public Entry(Source in) throws IOException {
try {
- BufferedSource source = Okio.buffer(Okio.source(in));
+ BufferedSource source = Okio.buffer(in);
url = source.readUtf8LineStrict();
requestMethod = source.readUtf8LineStrict();
Headers.Builder varyHeadersBuilder = new Headers.Builder();
int varyRequestHeaderLineCount = readInt(source);
for (int i = 0; i < varyRequestHeaderLineCount; i++) {
- varyHeadersBuilder.addLine(source.readUtf8LineStrict());
+ varyHeadersBuilder.addLenient(source.readUtf8LineStrict());
}
varyHeaders = varyHeadersBuilder.build();
- statusLine = source.readUtf8LineStrict();
+ StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
+ protocol = statusLine.protocol;
+ code = statusLine.code;
+ message = statusLine.message;
Headers.Builder responseHeadersBuilder = new Headers.Builder();
int responseHeaderLineCount = readInt(source);
for (int i = 0; i < responseHeaderLineCount; i++) {
- responseHeadersBuilder.addLine(source.readUtf8LineStrict());
+ responseHeadersBuilder.addLenient(source.readUtf8LineStrict());
}
responseHeaders = responseHeadersBuilder.build();
@@ -479,37 +518,50 @@
public Entry(Response response) {
this.url = response.request().urlString();
- this.varyHeaders = response.request().headers().getAll(response.getVaryFields());
+ this.varyHeaders = OkHeaders.varyHeaders(response);
this.requestMethod = response.request().method();
- this.statusLine = response.statusLine();
+ this.protocol = response.protocol();
+ this.code = response.code();
+ this.message = response.message();
this.responseHeaders = response.headers();
this.handshake = response.handshake();
}
public void writeTo(DiskLruCache.Editor editor) throws IOException {
- OutputStream out = editor.newOutputStream(ENTRY_METADATA);
- Writer writer = new BufferedWriter(new OutputStreamWriter(out, UTF_8));
+ BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));
- writer.write(url + '\n');
- writer.write(requestMethod + '\n');
- writer.write(Integer.toString(varyHeaders.size()) + '\n');
- for (int i = 0; i < varyHeaders.size(); i++) {
- writer.write(varyHeaders.name(i) + ": " + varyHeaders.value(i) + '\n');
+ sink.writeUtf8(url);
+ sink.writeByte('\n');
+ sink.writeUtf8(requestMethod);
+ sink.writeByte('\n');
+ sink.writeUtf8(Integer.toString(varyHeaders.size()));
+ sink.writeByte('\n');
+ for (int i = 0, size = varyHeaders.size(); i < size; i++) {
+ sink.writeUtf8(varyHeaders.name(i));
+ sink.writeUtf8(": ");
+ sink.writeUtf8(varyHeaders.value(i));
+ sink.writeByte('\n');
}
- writer.write(statusLine + '\n');
- writer.write(Integer.toString(responseHeaders.size()) + '\n');
- for (int i = 0; i < responseHeaders.size(); i++) {
- writer.write(responseHeaders.name(i) + ": " + responseHeaders.value(i) + '\n');
+ sink.writeUtf8(new StatusLine(protocol, code, message).toString());
+ sink.writeByte('\n');
+ sink.writeUtf8(Integer.toString(responseHeaders.size()));
+ sink.writeByte('\n');
+ for (int i = 0, size = responseHeaders.size(); i < size; i++) {
+ sink.writeUtf8(responseHeaders.name(i));
+ sink.writeUtf8(": ");
+ sink.writeUtf8(responseHeaders.value(i));
+ sink.writeByte('\n');
}
if (isHttps()) {
- writer.write('\n');
- writer.write(handshake.cipherSuite() + '\n');
- writeCertArray(writer, handshake.peerCertificates());
- writeCertArray(writer, handshake.localCertificates());
+ sink.writeByte('\n');
+ sink.writeUtf8(handshake.cipherSuite());
+ sink.writeByte('\n');
+ writeCertArray(sink, handshake.peerCertificates());
+ writeCertArray(sink, handshake.localCertificates());
}
- writer.close();
+ sink.close();
}
private boolean isHttps() {
@@ -522,7 +574,7 @@
try {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
- List<Certificate> result = new ArrayList<Certificate>(length);
+ List<Certificate> result = new ArrayList<>(length);
for (int i = 0; i < length; i++) {
String line = source.readUtf8LineStrict();
byte[] bytes = ByteString.decodeBase64(line).toByteArray();
@@ -534,13 +586,16 @@
}
}
- private void writeCertArray(Writer writer, List<Certificate> certificates) throws IOException {
+ private void writeCertArray(BufferedSink sink, List<Certificate> certificates)
+ throws IOException {
try {
- writer.write(Integer.toString(certificates.size()) + '\n');
+ sink.writeUtf8(Integer.toString(certificates.size()));
+ sink.writeByte('\n');
for (int i = 0, size = certificates.size(); i < size; i++) {
byte[] bytes = certificates.get(i).getEncoded();
String line = ByteString.of(bytes).base64();
- writer.write(line + '\n');
+ sink.writeUtf8(line);
+ sink.writeByte('\n');
}
} catch (CertificateEncodingException e) {
throw new IOException(e.getMessage());
@@ -550,15 +605,22 @@
public boolean matches(Request request, Response response) {
return url.equals(request.urlString())
&& requestMethod.equals(request.method())
- && response.varyMatches(varyHeaders, request);
+ && OkHeaders.varyMatches(response, varyHeaders, request);
}
public Response response(Request request, DiskLruCache.Snapshot snapshot) {
String contentType = responseHeaders.get("Content-Type");
String contentLength = responseHeaders.get("Content-Length");
+ Request cacheRequest = new Request.Builder()
+ .url(url)
+ .method(requestMethod, null)
+ .headers(varyHeaders)
+ .build();
return new Response.Builder()
- .request(request)
- .statusLine(statusLine)
+ .request(cacheRequest)
+ .protocol(protocol)
+ .code(code)
+ .message(message)
.headers(responseHeaders)
.body(new CacheResponseBody(snapshot, contentType, contentLength))
.handshake(handshake)
@@ -575,9 +637,9 @@
}
}
- private static class CacheResponseBody extends Response.Body {
+ private static class CacheResponseBody extends ResponseBody {
private final DiskLruCache.Snapshot snapshot;
- private final InputStream bodyIn;
+ private final BufferedSource bodySource;
private final String contentType;
private final String contentLength;
@@ -587,17 +649,13 @@
this.contentType = contentType;
this.contentLength = contentLength;
- // This input stream closes the snapshot when the stream is closed.
- this.bodyIn = new FilterInputStream(snapshot.getInputStream(ENTRY_BODY)) {
+ Source source = snapshot.getSource(ENTRY_BODY);
+ bodySource = Okio.buffer(new ForwardingSource(source) {
@Override public void close() throws IOException {
snapshot.close();
super.close();
}
- };
- }
-
- @Override public boolean ready() throws IOException {
- return true;
+ });
}
@Override public MediaType contentType() {
@@ -612,8 +670,8 @@
}
}
- @Override public InputStream byteStream() {
- return bodyIn;
+ @Override public BufferedSource source() {
+ return bodySource;
}
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java
index dc944e4..f7d9f30 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/CacheControl.java
@@ -1,6 +1,7 @@
package com.squareup.okhttp;
import com.squareup.okhttp.internal.http.HeaderParser;
+import java.util.concurrent.TimeUnit;
/**
* A Cache-Control header with cache directives from a server or client. These
@@ -11,6 +12,24 @@
* 2616, 14.9</a>.
*/
public final class CacheControl {
+ /**
+ * Cache control request directives that require network validation of
+ * responses. Note that such requests may be assisted by the cache via
+ * conditional GET requests.
+ */
+ public static final CacheControl FORCE_NETWORK = new Builder().noCache().build();
+
+ /**
+ * Cache control request directives that uses the cache only, even if the
+ * cached response is stale. If the response isn't available in the cache or
+ * requires server validation, the call will fail with a {@code 504
+ * Unsatisfiable Request}.
+ */
+ public static final CacheControl FORCE_CACHE = new Builder()
+ .onlyIfCached()
+ .maxStale(Integer.MAX_VALUE, TimeUnit.SECONDS)
+ .build();
+
private final boolean noCache;
private final boolean noStore;
private final int maxAgeSeconds;
@@ -20,10 +39,13 @@
private final int maxStaleSeconds;
private final int minFreshSeconds;
private final boolean onlyIfCached;
+ private final boolean noTransform;
+
+ String headerValue; // Lazily computed, if absent.
private CacheControl(boolean noCache, boolean noStore, int maxAgeSeconds, int sMaxAgeSeconds,
boolean isPublic, boolean mustRevalidate, int maxStaleSeconds, int minFreshSeconds,
- boolean onlyIfCached) {
+ boolean onlyIfCached, boolean noTransform, String headerValue) {
this.noCache = noCache;
this.noStore = noStore;
this.maxAgeSeconds = maxAgeSeconds;
@@ -33,6 +55,21 @@
this.maxStaleSeconds = maxStaleSeconds;
this.minFreshSeconds = minFreshSeconds;
this.onlyIfCached = onlyIfCached;
+ this.noTransform = noTransform;
+ this.headerValue = headerValue;
+ }
+
+ private CacheControl(Builder builder) {
+ this.noCache = builder.noCache;
+ this.noStore = builder.noStore;
+ this.maxAgeSeconds = builder.maxAgeSeconds;
+ this.sMaxAgeSeconds = -1;
+ this.isPublic = false;
+ this.mustRevalidate = false;
+ this.maxStaleSeconds = builder.maxStaleSeconds;
+ this.minFreshSeconds = builder.minFreshSeconds;
+ this.onlyIfCached = builder.onlyIfCached;
+ this.noTransform = builder.noTransform;
}
/**
@@ -96,6 +133,10 @@
return onlyIfCached;
}
+ public boolean noTransform() {
+ return noTransform;
+ }
+
/**
* Returns the cache directives of {@code headers}. This honors both
* Cache-Control and Pragma headers if they are present.
@@ -110,41 +151,56 @@
int maxStaleSeconds = -1;
int minFreshSeconds = -1;
boolean onlyIfCached = false;
+ boolean noTransform = false;
- for (int i = 0; i < headers.size(); i++) {
- if (!headers.name(i).equalsIgnoreCase("Cache-Control")
- && !headers.name(i).equalsIgnoreCase("Pragma")) {
+ boolean canUseHeaderValue = true;
+ String headerValue = null;
+
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ String name = headers.name(i);
+ String value = headers.value(i);
+
+ if (name.equalsIgnoreCase("Cache-Control")) {
+ if (headerValue != null) {
+ // Multiple cache-control headers means we can't use the raw value.
+ canUseHeaderValue = false;
+ } else {
+ headerValue = value;
+ }
+ } else if (name.equalsIgnoreCase("Pragma")) {
+ // Might specify additional cache-control params. We invalidate just in case.
+ canUseHeaderValue = false;
+ } else {
continue;
}
- String string = headers.value(i);
int pos = 0;
- while (pos < string.length()) {
+ while (pos < value.length()) {
int tokenStart = pos;
- pos = HeaderParser.skipUntil(string, pos, "=,;");
- String directive = string.substring(tokenStart, pos).trim();
+ pos = HeaderParser.skipUntil(value, pos, "=,;");
+ String directive = value.substring(tokenStart, pos).trim();
String parameter;
- if (pos == string.length() || string.charAt(pos) == ',' || string.charAt(pos) == ';') {
+ if (pos == value.length() || value.charAt(pos) == ',' || value.charAt(pos) == ';') {
pos++; // consume ',' or ';' (if necessary)
parameter = null;
} else {
pos++; // consume '='
- pos = HeaderParser.skipWhitespace(string, pos);
+ pos = HeaderParser.skipWhitespace(value, pos);
// quoted string
- if (pos < string.length() && string.charAt(pos) == '\"') {
+ if (pos < value.length() && value.charAt(pos) == '\"') {
pos++; // consume '"' open quote
int parameterStart = pos;
- pos = HeaderParser.skipUntil(string, pos, "\"");
- parameter = string.substring(parameterStart, pos);
+ pos = HeaderParser.skipUntil(value, pos, "\"");
+ parameter = value.substring(parameterStart, pos);
pos++; // consume '"' close quote (if necessary)
// unquoted string
} else {
int parameterStart = pos;
- pos = HeaderParser.skipUntil(string, pos, ",;");
- parameter = string.substring(parameterStart, pos).trim();
+ pos = HeaderParser.skipUntil(value, pos, ",;");
+ parameter = value.substring(parameterStart, pos).trim();
}
}
@@ -153,24 +209,147 @@
} else if ("no-store".equalsIgnoreCase(directive)) {
noStore = true;
} else if ("max-age".equalsIgnoreCase(directive)) {
- maxAgeSeconds = HeaderParser.parseSeconds(parameter);
+ maxAgeSeconds = HeaderParser.parseSeconds(parameter, -1);
} else if ("s-maxage".equalsIgnoreCase(directive)) {
- sMaxAgeSeconds = HeaderParser.parseSeconds(parameter);
+ sMaxAgeSeconds = HeaderParser.parseSeconds(parameter, -1);
} else if ("public".equalsIgnoreCase(directive)) {
isPublic = true;
} else if ("must-revalidate".equalsIgnoreCase(directive)) {
mustRevalidate = true;
} else if ("max-stale".equalsIgnoreCase(directive)) {
- maxStaleSeconds = HeaderParser.parseSeconds(parameter);
+ maxStaleSeconds = HeaderParser.parseSeconds(parameter, Integer.MAX_VALUE);
} else if ("min-fresh".equalsIgnoreCase(directive)) {
- minFreshSeconds = HeaderParser.parseSeconds(parameter);
+ minFreshSeconds = HeaderParser.parseSeconds(parameter, -1);
} else if ("only-if-cached".equalsIgnoreCase(directive)) {
onlyIfCached = true;
+ } else if ("no-transform".equalsIgnoreCase(directive)) {
+ noTransform = true;
}
}
}
+ if (!canUseHeaderValue) {
+ headerValue = null;
+ }
return new CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPublic,
- mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached);
+ mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, headerValue);
+ }
+
+ @Override public String toString() {
+ String result = headerValue;
+ return result != null ? result : (headerValue = headerValue());
+ }
+
+ private String headerValue() {
+ StringBuilder result = new StringBuilder();
+ if (noCache) result.append("no-cache, ");
+ if (noStore) result.append("no-store, ");
+ if (maxAgeSeconds != -1) result.append("max-age=").append(maxAgeSeconds).append(", ");
+ if (sMaxAgeSeconds != -1) result.append("s-maxage=").append(sMaxAgeSeconds).append(", ");
+ if (isPublic) result.append("public, ");
+ if (mustRevalidate) result.append("must-revalidate, ");
+ if (maxStaleSeconds != -1) result.append("max-stale=").append(maxStaleSeconds).append(", ");
+ if (minFreshSeconds != -1) result.append("min-fresh=").append(minFreshSeconds).append(", ");
+ if (onlyIfCached) result.append("only-if-cached, ");
+ if (noTransform) result.append("no-transform, ");
+ if (result.length() == 0) return "";
+ result.delete(result.length() - 2, result.length());
+ return result.toString();
+ }
+
+ /** Builds a {@code Cache-Control} request header. */
+ public static final class Builder {
+ boolean noCache;
+ boolean noStore;
+ int maxAgeSeconds = -1;
+ int maxStaleSeconds = -1;
+ int minFreshSeconds = -1;
+ boolean onlyIfCached;
+ boolean noTransform;
+
+ /** Don't accept an unvalidated cached response. */
+ public Builder noCache() {
+ this.noCache = true;
+ return this;
+ }
+
+ /** Don't store the server's response in any cache. */
+ public Builder noStore() {
+ this.noStore = true;
+ return this;
+ }
+
+ /**
+ * Sets the maximum age of a cached response. If the cache response's age
+ * exceeds {@code maxAge}, it will not be used and a network request will
+ * be made.
+ *
+ * @param maxAge a non-negative integer. This is stored and transmitted with
+ * {@link TimeUnit#SECONDS} precision; finer precision will be lost.
+ */
+ public Builder maxAge(int maxAge, TimeUnit timeUnit) {
+ if (maxAge < 0) throw new IllegalArgumentException("maxAge < 0: " + maxAge);
+ long maxAgeSecondsLong = timeUnit.toSeconds(maxAge);
+ this.maxAgeSeconds = maxAgeSecondsLong > Integer.MAX_VALUE
+ ? Integer.MAX_VALUE
+ : (int) maxAgeSecondsLong;
+ return this;
+ }
+
+ /**
+ * Accept cached responses that have exceeded their freshness lifetime by
+ * up to {@code maxStale}. If unspecified, stale cache responses will not be
+ * used.
+ *
+ * @param maxStale a non-negative integer. This is stored and transmitted
+ * with {@link TimeUnit#SECONDS} precision; finer precision will be
+ * lost.
+ */
+ public Builder maxStale(int maxStale, TimeUnit timeUnit) {
+ if (maxStale < 0) throw new IllegalArgumentException("maxStale < 0: " + maxStale);
+ long maxStaleSecondsLong = timeUnit.toSeconds(maxStale);
+ this.maxStaleSeconds = maxStaleSecondsLong > Integer.MAX_VALUE
+ ? Integer.MAX_VALUE
+ : (int) maxStaleSecondsLong;
+ return this;
+ }
+
+ /**
+ * Sets the minimum number of seconds that a response will continue to be
+ * fresh for. If the response will be stale when {@code minFresh} have
+ * elapsed, the cached response will not be used and a network request will
+ * be made.
+ *
+ * @param minFresh a non-negative integer. This is stored and transmitted
+ * with {@link TimeUnit#SECONDS} precision; finer precision will be
+ * lost.
+ */
+ public Builder minFresh(int minFresh, TimeUnit timeUnit) {
+ if (minFresh < 0) throw new IllegalArgumentException("minFresh < 0: " + minFresh);
+ long minFreshSecondsLong = timeUnit.toSeconds(minFresh);
+ this.minFreshSeconds = minFreshSecondsLong > Integer.MAX_VALUE
+ ? Integer.MAX_VALUE
+ : (int) minFreshSecondsLong;
+ return this;
+ }
+
+ /**
+ * Only accept the response if it is in the cache. If the response isn't
+ * cached, a {@code 504 Unsatisfiable Request} response will be returned.
+ */
+ public Builder onlyIfCached() {
+ this.onlyIfCached = true;
+ return this;
+ }
+
+ /** Don't accept a transformed response. */
+ public Builder noTransform() {
+ this.noTransform = true;
+ return this;
+ }
+
+ public CacheControl build() {
+ return new CacheControl(this);
+ }
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
new file mode 100644
index 0000000..c4742c2
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2014 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.NamedRunnable;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.logging.Level;
+
+import static com.squareup.okhttp.internal.Internal.logger;
+import static com.squareup.okhttp.internal.http.HttpEngine.MAX_FOLLOW_UPS;
+
+/**
+ * A call is a request that has been prepared for execution. A call can be
+ * canceled. As this object represents a single request/response pair (stream),
+ * it cannot be executed twice.
+ */
+public class Call {
+ private final OkHttpClient client;
+
+ // Guarded by this.
+ private boolean executed;
+ volatile boolean canceled;
+
+ /** The application's original request unadulterated by redirects or auth headers. */
+ Request originalRequest;
+ HttpEngine engine;
+
+ protected Call(OkHttpClient client, Request originalRequest) {
+ // Copy the client. Otherwise changes (socket factory, redirect policy,
+ // etc.) may incorrectly be reflected in the request when it is executed.
+ this.client = client.copyWithDefaults();
+ this.originalRequest = originalRequest;
+ }
+
+ /**
+ * Invokes the request immediately, and blocks until the response can be
+ * processed or is in error.
+ *
+ * <p>The caller may read the response body with the response's
+ * {@link Response#body} method. To facilitate connection recycling, callers
+ * should always {@link ResponseBody#close() close the response body}.
+ *
+ * <p>Note that transport-layer success (receiving a HTTP response code,
+ * headers and body) does not necessarily indicate application-layer success:
+ * {@code response} may still indicate an unhappy HTTP response code like 404
+ * or 500.
+ *
+ * @throws IOException if the request could not be executed due to
+ * cancellation, a connectivity problem or timeout. Because networks can
+ * fail during an exchange, it is possible that the remote server
+ * accepted the request before the failure.
+ *
+ * @throws IllegalStateException when the call has already been executed.
+ */
+ public Response execute() throws IOException {
+ synchronized (this) {
+ if (executed) throw new IllegalStateException("Already Executed");
+ executed = true;
+ }
+ try {
+ client.getDispatcher().executed(this);
+ Response result = getResponseWithInterceptorChain(false);
+ if (result == null) throw new IOException("Canceled");
+ return result;
+ } finally {
+ client.getDispatcher().finished(this);
+ }
+ }
+
+ Object tag() {
+ return originalRequest.tag();
+ }
+
+ /**
+ * Schedules the request to be executed at some point in the future.
+ *
+ * <p>The {@link OkHttpClient#getDispatcher dispatcher} defines when the
+ * request will run: usually immediately unless there are several other
+ * requests currently being executed.
+ *
+ * <p>This client will later call back {@code responseCallback} with either
+ * an HTTP response or a failure exception. If you {@link #cancel} a request
+ * before it completes the callback will not be invoked.
+ *
+ * @throws IllegalStateException when the call has already been executed.
+ */
+ public void enqueue(Callback responseCallback) {
+ enqueue(responseCallback, false);
+ }
+
+ void enqueue(Callback responseCallback, boolean forWebSocket) {
+ synchronized (this) {
+ if (executed) throw new IllegalStateException("Already Executed");
+ executed = true;
+ }
+ client.getDispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
+ }
+
+ /**
+ * Cancels the request, if possible. Requests that are already complete
+ * cannot be canceled.
+ */
+ public void cancel() {
+ canceled = true;
+ if (engine != null) engine.disconnect();
+ }
+
+ public boolean isCanceled() {
+ return canceled;
+ }
+
+ final class AsyncCall extends NamedRunnable {
+ private final Callback responseCallback;
+ private final boolean forWebSocket;
+
+ private AsyncCall(Callback responseCallback, boolean forWebSocket) {
+ super("OkHttp %s", originalRequest.urlString());
+ this.responseCallback = responseCallback;
+ this.forWebSocket = forWebSocket;
+ }
+
+ String host() {
+ return originalRequest.url().getHost();
+ }
+
+ Request request() {
+ return originalRequest;
+ }
+
+ Object tag() {
+ return originalRequest.tag();
+ }
+
+ void cancel() {
+ Call.this.cancel();
+ }
+
+ Call get() {
+ return Call.this;
+ }
+
+ @Override protected void execute() {
+ boolean signalledCallback = false;
+ try {
+ Response response = getResponseWithInterceptorChain(forWebSocket);
+ if (canceled) {
+ signalledCallback = true;
+ responseCallback.onFailure(originalRequest, new IOException("Canceled"));
+ } else {
+ signalledCallback = true;
+ responseCallback.onResponse(response);
+ }
+ } catch (IOException e) {
+ if (signalledCallback) {
+ // Do not signal the callback twice!
+ logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
+ } else {
+ responseCallback.onFailure(engine.getRequest(), e);
+ }
+ } finally {
+ client.getDispatcher().finished(this);
+ }
+ }
+ }
+
+ /**
+ * Returns a string that describes this call. Doesn't include a full URL as that might contain
+ * sensitive information.
+ */
+ private String toLoggableString() {
+ String string = canceled ? "canceled call" : "call";
+ try {
+ String redactedUrl = new URL(originalRequest.url(), "/...").toString();
+ return string + " to " + redactedUrl;
+ } catch (MalformedURLException e) {
+ return string;
+ }
+ }
+
+ private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
+ Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
+ return chain.proceed(originalRequest);
+ }
+
+ class ApplicationInterceptorChain implements Interceptor.Chain {
+ private final int index;
+ private final Request request;
+ private final boolean forWebSocket;
+
+ ApplicationInterceptorChain(int index, Request request, boolean forWebSocket) {
+ this.index = index;
+ this.request = request;
+ this.forWebSocket = forWebSocket;
+ }
+
+ @Override public Connection connection() {
+ return null;
+ }
+
+ @Override public Request request() {
+ return request;
+ }
+
+ @Override public Response proceed(Request request) throws IOException {
+ 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);
+ }
+ }
+ }
+
+ /**
+ * Performs the request and returns the response. May return null if this
+ * call was canceled.
+ */
+ Response getResponse(Request request, boolean forWebSocket) throws IOException {
+ // Copy body metadata to the appropriate request headers.
+ RequestBody body = request.body();
+ if (body != null) {
+ Request.Builder requestBuilder = request.newBuilder();
+
+ MediaType contentType = body.contentType();
+ if (contentType != null) {
+ requestBuilder.header("Content-Type", contentType.toString());
+ }
+
+ long contentLength = body.contentLength();
+ if (contentLength != -1) {
+ requestBuilder.header("Content-Length", Long.toString(contentLength));
+ requestBuilder.removeHeader("Transfer-Encoding");
+ } else {
+ requestBuilder.header("Transfer-Encoding", "chunked");
+ requestBuilder.removeHeader("Content-Length");
+ }
+
+ request = requestBuilder.build();
+ }
+
+ // 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);
+
+ int followUpCount = 0;
+ while (true) {
+ if (canceled) {
+ engine.releaseConnection();
+ return null;
+ }
+
+ try {
+ engine.sendRequest();
+ engine.readResponse();
+ } catch (IOException e) {
+ HttpEngine retryEngine = engine.recover(e, null);
+ if (retryEngine != null) {
+ engine = retryEngine;
+ continue;
+ }
+
+ // Give up; recovery is not possible.
+ throw e;
+ }
+
+ Response response = engine.getResponse();
+ Request followUp = engine.followUpRequest();
+
+ if (followUp == null) {
+ if (!forWebSocket) {
+ engine.releaseConnection();
+ }
+ return response;
+ }
+
+ if (++followUpCount > MAX_FOLLOW_UPS) {
+ throw new ProtocolException("Too many follow-up requests: " + followUpCount);
+ }
+
+ if (!engine.sameConnection(followUp.url())) {
+ engine.releaseConnection();
+ }
+
+ Connection connection = engine.close();
+ request = followUp;
+ engine = new HttpEngine(client, request, false, false, forWebSocket, connection, null, null,
+ response);
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Callback.java b/okhttp/src/main/java/com/squareup/okhttp/Callback.java
new file mode 100644
index 0000000..d86960f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Callback.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 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.io.IOException;
+
+public interface Callback {
+ /**
+ * Called when the request could not be executed due to cancellation, a
+ * connectivity problem or timeout. Because networks can fail during an
+ * exchange, it is possible that the remote server accepted the request
+ * before the failure.
+ */
+ void onFailure(Request request, IOException e);
+
+ /**
+ * Called when the HTTP response was successfully returned by the remote
+ * server. The callback may proceed to read the response body with {@link
+ * Response#body}. The response is still live until its response body is
+ * closed with {@code response.body().close()}. The recipient of the callback
+ * may even consume the response body on another thread.
+ *
+ * <p>Note that transport-layer success (receiving a HTTP response code,
+ * headers and body) does not necessarily indicate application-layer
+ * success: {@code response} may still indicate an unhappy HTTP response
+ * code like 404 or 500.
+ */
+ void onResponse(Response response) throws IOException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
new file mode 100644
index 0000000..2c5a2af
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014 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.Util;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import okio.ByteString;
+
+import static java.util.Collections.unmodifiableList;
+
+/**
+ * Constrains which certificates are trusted. Pinning certificates defends
+ * against attacks on certificate authorities. It also prevents connections
+ * through man-in-the-middle certificate authorities either known or unknown to
+ * the application's user.
+ *
+ * <p>This class currently pins a certificate's Subject Public Key Info as
+ * described on <a href="http://goo.gl/AIx3e5">Adam Langley's Weblog</a>. Pins
+ * are base-64 SHA-1 hashes, consistent with the format Chromium uses for <a
+ * href="http://goo.gl/XDh6je">static certificates</a>. See Chromium's <a
+ * href="http://goo.gl/4CCnGs">pinsets</a> for hostnames that are pinned in that
+ * browser.
+ *
+ * <h3>Setting up Certificate Pinning</h3>
+ * The easiest way to pin a host is turn on pinning with a broken configuration
+ * and read the expected configuration when the connection fails. Be sure to
+ * do this on a trusted network, and without man-in-the-middle tools like <a
+ * href="http://charlesproxy.com">Charles</a> or <a
+ * href="http://fiddlertool.com">Fiddler</a>.
+ *
+ * <p>For example, to pin {@code https://publicobject.com}, start with a broken
+ * configuration: <pre> {@code
+ *
+ * String hostname = "publicobject.com";
+ * CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ * .add(hostname, "sha1/BOGUSPIN")
+ * .build();
+ * OkHttpClient client = new OkHttpClient();
+ * client.setCertificatePinner(certificatePinner);
+ *
+ * Request request = new Request.Builder()
+ * .url("https://" + hostname)
+ * .build();
+ * client.newCall(request).execute();
+ * }</pre>
+ *
+ * As expected, this fails with a certificate pinning exception: <pre> {@code
+ *
+ * javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
+ * Peer certificate chain:
+ * sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=: CN=publicobject.com, OU=PositiveSSL
+ * sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=: CN=COMODO RSA Domain Validation Secure Server CA
+ * sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
+ * sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
+ * Pinned certificates for publicobject.com:
+ * sha1/BOGUSPIN
+ * at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
+ * at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
+ * at com.squareup.okhttp.Connection.connect(Connection.java)
+ * at com.squareup.okhttp.Connection.connectAndSetOwner(Connection.java)
+ * }</pre>
+ *
+ * Follow up by pasting the public key hashes from the exception into the
+ * certificate pinner's configuration: <pre> {@code
+ *
+ * CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ * .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
+ * .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
+ * .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
+ * .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
+ * .build();
+ * }</pre>
+ *
+ * Pinning is per-hostname. To pin both {@code publicobject.com} and {@code
+ * www.publicobject.com}, you must configure both hostnames.
+ *
+ * <h3>Warning: Certificate Pinning is Dangerous!</h3>
+ * Pinning certificates limits your server team's abilities to update their TLS
+ * certificates. By pinning certificates, you take on additional operational
+ * complexity and limit your ability to migrate between certificate authorities.
+ * Do not use certificate pinning without the blessing of your server's TLS
+ * administrator!
+ */
+public final class CertificatePinner {
+ public static final CertificatePinner DEFAULT = new Builder().build();
+
+ private final Map<String, List<ByteString>> hostnameToPins;
+
+ private CertificatePinner(Builder builder) {
+ hostnameToPins = Util.immutableMap(builder.hostnameToPins);
+ }
+
+ /**
+ * Confirms that at least one of the certificates pinned for {@code hostname}
+ * is in {@code peerCertificates}. Does nothing if there are no certificates
+ * pinned for {@code hostname}. OkHttp calls this after a successful TLS
+ * handshake, but before the connection is used.
+ *
+ * @throws SSLPeerUnverifiedException if {@code peerCertificates} don't match
+ * the certificates pinned for {@code hostname}.
+ */
+ public void check(String hostname, List<Certificate> peerCertificates)
+ throws SSLPeerUnverifiedException {
+ List<ByteString> pins = hostnameToPins.get(hostname);
+ if (pins == null) return;
+
+ for (int i = 0, size = peerCertificates.size(); i < size; i++) {
+ X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
+ if (pins.contains(sha1(x509Certificate))) return; // Success!
+ }
+
+ // If we couldn't find a matching pin, format a nice exception.
+ StringBuilder message = new StringBuilder()
+ .append("Certificate pinning failure!")
+ .append("\n Peer certificate chain:");
+ for (int i = 0, size = peerCertificates.size(); i < size; i++) {
+ X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
+ message.append("\n ").append(pin(x509Certificate))
+ .append(": ").append(x509Certificate.getSubjectDN().getName());
+ }
+ message.append("\n Pinned certificates for ").append(hostname).append(":");
+ for (int i = 0, size = pins.size(); i < size; i++) {
+ ByteString pin = pins.get(i);
+ message.append("\n sha1/").append(pin.base64());
+ }
+ throw new SSLPeerUnverifiedException(message.toString());
+ }
+
+ /** @deprecated replaced with {@link #check(String, List)}. */
+ public void check(String hostname, Certificate... peerCertificates)
+ throws SSLPeerUnverifiedException {
+ check(hostname, Arrays.asList(peerCertificates));
+ }
+
+ /**
+ * Returns the SHA-1 of {@code certificate}'s public key. This uses the
+ * mechanism Moxie Marlinspike describes in <a
+ * href="https://github.com/moxie0/AndroidPinning">Android Pinning</a>.
+ */
+ public static String pin(Certificate certificate) {
+ if (!(certificate instanceof X509Certificate)) {
+ throw new IllegalArgumentException("Certificate pinning requires X509 certificates");
+ }
+ return "sha1/" + sha1((X509Certificate) certificate).base64();
+ }
+
+ private static ByteString sha1(X509Certificate x509Certificate) {
+ return Util.sha1(ByteString.of(x509Certificate.getPublicKey().getEncoded()));
+ }
+
+ /** Builds a configured certificate pinner. */
+ public static final class Builder {
+ private final Map<String, List<ByteString>> hostnameToPins = new LinkedHashMap<>();
+
+ /**
+ * Pins certificates for {@code hostname}. Each pin is a SHA-1 hash of a
+ * certificate's Subject Public Key Info, base64-encoded and prefixed with
+ * "sha1/".
+ */
+ public Builder add(String hostname, String... pins) {
+ if (hostname == null) throw new IllegalArgumentException("hostname == null");
+
+ List<ByteString> hostPins = new ArrayList<>();
+ List<ByteString> previousPins = hostnameToPins.put(hostname, unmodifiableList(hostPins));
+ if (previousPins != null) {
+ hostPins.addAll(previousPins);
+ }
+
+ for (String pin : pins) {
+ if (!pin.startsWith("sha1/")) {
+ throw new IllegalArgumentException("pins must start with 'sha1/': " + pin);
+ }
+ ByteString decodedPin = ByteString.decodeBase64(pin.substring("sha1/".length()));
+ if (decodedPin == null) {
+ throw new IllegalArgumentException("pins must be base64: " + pin);
+ }
+ hostPins.add(decodedPin);
+ }
+
+ return this;
+ }
+
+ public CertificatePinner build() {
+ return new CertificatePinner(this);
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Challenge.java b/okhttp/src/main/java/com/squareup/okhttp/Challenge.java
new file mode 100644
index 0000000..a1ef714
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Challenge.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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 static com.squareup.okhttp.internal.Util.equal;
+
+/** An RFC 2617 challenge. */
+public final class Challenge {
+ private final String scheme;
+ private final String realm;
+
+ public Challenge(String scheme, String realm) {
+ this.scheme = scheme;
+ this.realm = realm;
+ }
+
+ /** Returns the authentication scheme, like {@code Basic}. */
+ public String getScheme() {
+ return scheme;
+ }
+
+ /** Returns the protection space. */
+ public String getRealm() {
+ return realm;
+ }
+
+ @Override public boolean equals(Object o) {
+ return o instanceof Challenge
+ && equal(scheme, ((Challenge) o).scheme)
+ && equal(realm, ((Challenge) o).realm);
+ }
+
+ @Override public int hashCode() {
+ int result = 29;
+ result = 31 * result + (realm != null ? realm.hashCode() : 0);
+ result = 31 * result + (scheme != null ? scheme.hashCode() : 0);
+ return result;
+ }
+
+ @Override public String toString() {
+ return scheme + " realm=\"" + realm + "\"";
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CipherSuite.java b/okhttp/src/main/java/com/squareup/okhttp/CipherSuite.java
new file mode 100644
index 0000000..1334457
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/CipherSuite.java
@@ -0,0 +1,375 @@
+/*
+ * Copyright (C) 2014 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 static java.lang.Integer.MAX_VALUE;
+
+/**
+ * <a href="https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml">TLS cipher
+ * suites</a>.
+ *
+ * <p><strong>Not all cipher suites are supported on all platforms.</strong> As newer cipher suites
+ * are created (for stronger privacy, better performance, etc.) they will be adopted by the platform
+ * and then exposed here. Cipher suites that are not available on either Android (through API level
+ * 20) or Java (through JDK 8) are omitted for brevity.
+ *
+ * <p>See also <a href="https://android.googlesource.com/platform/external/conscrypt/+/master/src/main/java/org/conscrypt/NativeCrypto.java">NativeCrypto.java</a>
+ * from conscrypt, which lists the cipher suites supported by Android.
+ */
+public enum CipherSuite {
+ // Last updated 2014-11-11 using cipher suites from Android 21 and Java 8.
+
+ // TLS_NULL_WITH_NULL_NULL("TLS_NULL_WITH_NULL_NULL", 0x0000, 5246, MAX_VALUE, MAX_VALUE),
+ TLS_RSA_WITH_NULL_MD5("SSL_RSA_WITH_NULL_MD5", 0x0001, 5246, 6, 10),
+ TLS_RSA_WITH_NULL_SHA("SSL_RSA_WITH_NULL_SHA", 0x0002, 5246, 6, 10),
+ TLS_RSA_EXPORT_WITH_RC4_40_MD5("SSL_RSA_EXPORT_WITH_RC4_40_MD5", 0x0003, 4346, 6, 10),
+ TLS_RSA_WITH_RC4_128_MD5("SSL_RSA_WITH_RC4_128_MD5", 0x0004, 5246, 6, 10),
+ TLS_RSA_WITH_RC4_128_SHA("SSL_RSA_WITH_RC4_128_SHA", 0x0005, 5246, 6, 10),
+ // TLS_RSA_EXPORT_WITH_RC2_CBC_40_MD5("SSL_RSA_EXPORT_WITH_RC2_CBC_40_MD5", 0x0006, 4346, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_IDEA_CBC_SHA("TLS_RSA_WITH_IDEA_CBC_SHA", 0x0007, 5469, MAX_VALUE, MAX_VALUE),
+ TLS_RSA_EXPORT_WITH_DES40_CBC_SHA("SSL_RSA_EXPORT_WITH_DES40_CBC_SHA", 0x0008, 4346, 6, 10),
+ TLS_RSA_WITH_DES_CBC_SHA("SSL_RSA_WITH_DES_CBC_SHA", 0x0009, 5469, 6, 10),
+ TLS_RSA_WITH_3DES_EDE_CBC_SHA("SSL_RSA_WITH_3DES_EDE_CBC_SHA", 0x000a, 5246, 6, 10),
+ // TLS_DH_DSS_EXPORT_WITH_DES40_CBC_SHA("SSL_DH_DSS_EXPORT_WITH_DES40_CBC_SHA", 0x000b, 4346, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_DES_CBC_SHA("TLS_DH_DSS_WITH_DES_CBC_SHA", 0x000c, 5469, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA("TLS_DH_DSS_WITH_3DES_EDE_CBC_SHA", 0x000d, 5246, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_EXPORT_WITH_DES40_CBC_SHA("SSL_DH_RSA_EXPORT_WITH_DES40_CBC_SHA", 0x000e, 4346, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_DES_CBC_SHA("TLS_DH_RSA_WITH_DES_CBC_SHA", 0x000f, 5469, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA("TLS_DH_RSA_WITH_3DES_EDE_CBC_SHA", 0x0010, 5246, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA("SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA", 0x0011, 4346, 6, 10),
+ TLS_DHE_DSS_WITH_DES_CBC_SHA("SSL_DHE_DSS_WITH_DES_CBC_SHA", 0x0012, 5469, 6, 10),
+ TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA("SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA", 0x0013, 5246, 6, 10),
+ TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA("SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA", 0x0014, 4346, 6, 10),
+ TLS_DHE_RSA_WITH_DES_CBC_SHA("SSL_DHE_RSA_WITH_DES_CBC_SHA", 0x0015, 5469, 6, 10),
+ TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA("SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA", 0x0016, 5246, 6, 10),
+ TLS_DH_anon_EXPORT_WITH_RC4_40_MD5("SSL_DH_anon_EXPORT_WITH_RC4_40_MD5", 0x0017, 4346, 6, 10),
+ TLS_DH_anon_WITH_RC4_128_MD5("SSL_DH_anon_WITH_RC4_128_MD5", 0x0018, 5246, 6, 10),
+ TLS_DH_anon_EXPORT_WITH_DES40_CBC_SHA("SSL_DH_anon_EXPORT_WITH_DES40_CBC_SHA", 0x0019, 4346, 6, 10),
+ TLS_DH_anon_WITH_DES_CBC_SHA("SSL_DH_anon_WITH_DES_CBC_SHA", 0x001a, 5469, 6, 10),
+ TLS_DH_anon_WITH_3DES_EDE_CBC_SHA("SSL_DH_anon_WITH_3DES_EDE_CBC_SHA", 0x001b, 5246, 6, 10),
+ TLS_KRB5_WITH_DES_CBC_SHA("TLS_KRB5_WITH_DES_CBC_SHA", 0x001e, 2712, 6, MAX_VALUE),
+ TLS_KRB5_WITH_3DES_EDE_CBC_SHA("TLS_KRB5_WITH_3DES_EDE_CBC_SHA", 0x001f, 2712, 6, MAX_VALUE),
+ TLS_KRB5_WITH_RC4_128_SHA("TLS_KRB5_WITH_RC4_128_SHA", 0x0020, 2712, 6, MAX_VALUE),
+ // TLS_KRB5_WITH_IDEA_CBC_SHA("TLS_KRB5_WITH_IDEA_CBC_SHA", 0x0021, 2712, MAX_VALUE, MAX_VALUE),
+ TLS_KRB5_WITH_DES_CBC_MD5("TLS_KRB5_WITH_DES_CBC_MD5", 0x0022, 2712, 6, MAX_VALUE),
+ TLS_KRB5_WITH_3DES_EDE_CBC_MD5("TLS_KRB5_WITH_3DES_EDE_CBC_MD5", 0x0023, 2712, 6, MAX_VALUE),
+ TLS_KRB5_WITH_RC4_128_MD5("TLS_KRB5_WITH_RC4_128_MD5", 0x0024, 2712, 6, MAX_VALUE),
+ // TLS_KRB5_WITH_IDEA_CBC_MD5("TLS_KRB5_WITH_IDEA_CBC_MD5", 0x0025, 2712, MAX_VALUE, MAX_VALUE),
+ TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA("TLS_KRB5_EXPORT_WITH_DES_CBC_40_SHA", 0x0026, 2712, 6, MAX_VALUE),
+ // TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_SHA", 0x0027, 2712, MAX_VALUE, MAX_VALUE),
+ TLS_KRB5_EXPORT_WITH_RC4_40_SHA("TLS_KRB5_EXPORT_WITH_RC4_40_SHA", 0x0028, 2712, 6, MAX_VALUE),
+ TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5("TLS_KRB5_EXPORT_WITH_DES_CBC_40_MD5", 0x0029, 2712, 6, MAX_VALUE),
+ // TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5("TLS_KRB5_EXPORT_WITH_RC2_CBC_40_MD5", 0x002a, 2712, MAX_VALUE, MAX_VALUE),
+ TLS_KRB5_EXPORT_WITH_RC4_40_MD5("TLS_KRB5_EXPORT_WITH_RC4_40_MD5", 0x002b, 2712, 6, MAX_VALUE),
+ // TLS_PSK_WITH_NULL_SHA("TLS_PSK_WITH_NULL_SHA", 0x002c, 4785, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_NULL_SHA("TLS_DHE_PSK_WITH_NULL_SHA", 0x002d, 4785, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_NULL_SHA("TLS_RSA_PSK_WITH_NULL_SHA", 0x002e, 4785, MAX_VALUE, MAX_VALUE),
+ TLS_RSA_WITH_AES_128_CBC_SHA("TLS_RSA_WITH_AES_128_CBC_SHA", 0x002f, 5246, 6, 10),
+ // TLS_DH_DSS_WITH_AES_128_CBC_SHA("TLS_DH_DSS_WITH_AES_128_CBC_SHA", 0x0030, 5246, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_AES_128_CBC_SHA("TLS_DH_RSA_WITH_AES_128_CBC_SHA", 0x0031, 5246, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_DSS_WITH_AES_128_CBC_SHA("TLS_DHE_DSS_WITH_AES_128_CBC_SHA", 0x0032, 5246, 6, 10),
+ TLS_DHE_RSA_WITH_AES_128_CBC_SHA("TLS_DHE_RSA_WITH_AES_128_CBC_SHA", 0x0033, 5246, 6, 10),
+ TLS_DH_anon_WITH_AES_128_CBC_SHA("TLS_DH_anon_WITH_AES_128_CBC_SHA", 0x0034, 5246, 6, 10),
+ TLS_RSA_WITH_AES_256_CBC_SHA("TLS_RSA_WITH_AES_256_CBC_SHA", 0x0035, 5246, 6, 10),
+ // TLS_DH_DSS_WITH_AES_256_CBC_SHA("TLS_DH_DSS_WITH_AES_256_CBC_SHA", 0x0036, 5246, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_AES_256_CBC_SHA("TLS_DH_RSA_WITH_AES_256_CBC_SHA", 0x0037, 5246, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_DSS_WITH_AES_256_CBC_SHA("TLS_DHE_DSS_WITH_AES_256_CBC_SHA", 0x0038, 5246, 6, 10),
+ TLS_DHE_RSA_WITH_AES_256_CBC_SHA("TLS_DHE_RSA_WITH_AES_256_CBC_SHA", 0x0039, 5246, 6, 10),
+ TLS_DH_anon_WITH_AES_256_CBC_SHA("TLS_DH_anon_WITH_AES_256_CBC_SHA", 0x003a, 5246, 6, 10),
+ TLS_RSA_WITH_NULL_SHA256("TLS_RSA_WITH_NULL_SHA256", 0x003b, 5246, 7, 21),
+ TLS_RSA_WITH_AES_128_CBC_SHA256("TLS_RSA_WITH_AES_128_CBC_SHA256", 0x003c, 5246, 7, 21),
+ TLS_RSA_WITH_AES_256_CBC_SHA256("TLS_RSA_WITH_AES_256_CBC_SHA256", 0x003d, 5246, 7, 21),
+ // TLS_DH_DSS_WITH_AES_128_CBC_SHA256("TLS_DH_DSS_WITH_AES_128_CBC_SHA256", 0x003e, 5246, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_AES_128_CBC_SHA256("TLS_DH_RSA_WITH_AES_128_CBC_SHA256", 0x003f, 5246, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_DSS_WITH_AES_128_CBC_SHA256("TLS_DHE_DSS_WITH_AES_128_CBC_SHA256", 0x0040, 5246, 7, 21),
+ // TLS_RSA_WITH_CAMELLIA_128_CBC_SHA("TLS_RSA_WITH_CAMELLIA_128_CBC_SHA", 0x0041, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA("TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA", 0x0042, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA("TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA", 0x0043, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA("TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA", 0x0044, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA("TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA", 0x0045, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA("TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA", 0x0046, 5932, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_RSA_WITH_AES_128_CBC_SHA256("TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", 0x0067, 5246, 7, 21),
+ // TLS_DH_DSS_WITH_AES_256_CBC_SHA256("TLS_DH_DSS_WITH_AES_256_CBC_SHA256", 0x0068, 5246, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_AES_256_CBC_SHA256("TLS_DH_RSA_WITH_AES_256_CBC_SHA256", 0x0069, 5246, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_DSS_WITH_AES_256_CBC_SHA256("TLS_DHE_DSS_WITH_AES_256_CBC_SHA256", 0x006a, 5246, 7, 21),
+ TLS_DHE_RSA_WITH_AES_256_CBC_SHA256("TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", 0x006b, 5246, 7, 21),
+ TLS_DH_anon_WITH_AES_128_CBC_SHA256("TLS_DH_anon_WITH_AES_128_CBC_SHA256", 0x006c, 5246, 7, 21),
+ TLS_DH_anon_WITH_AES_256_CBC_SHA256("TLS_DH_anon_WITH_AES_256_CBC_SHA256", 0x006d, 5246, 7, 21),
+ // TLS_RSA_WITH_CAMELLIA_256_CBC_SHA("TLS_RSA_WITH_CAMELLIA_256_CBC_SHA", 0x0084, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA("TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA", 0x0085, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA("TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA", 0x0086, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA("TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA", 0x0087, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA("TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA", 0x0088, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA("TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA", 0x0089, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_RC4_128_SHA("TLS_PSK_WITH_RC4_128_SHA", 0x008a, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_3DES_EDE_CBC_SHA("TLS_PSK_WITH_3DES_EDE_CBC_SHA", 0x008b, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_128_CBC_SHA("TLS_PSK_WITH_AES_128_CBC_SHA", 0x008c, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_256_CBC_SHA("TLS_PSK_WITH_AES_256_CBC_SHA", 0x008d, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_RC4_128_SHA("TLS_DHE_PSK_WITH_RC4_128_SHA", 0x008e, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA("TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA", 0x008f, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_128_CBC_SHA("TLS_DHE_PSK_WITH_AES_128_CBC_SHA", 0x0090, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_256_CBC_SHA("TLS_DHE_PSK_WITH_AES_256_CBC_SHA", 0x0091, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_RC4_128_SHA("TLS_RSA_PSK_WITH_RC4_128_SHA", 0x0092, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA("TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA", 0x0093, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_AES_128_CBC_SHA("TLS_RSA_PSK_WITH_AES_128_CBC_SHA", 0x0094, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_AES_256_CBC_SHA("TLS_RSA_PSK_WITH_AES_256_CBC_SHA", 0x0095, 4279, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_SEED_CBC_SHA("TLS_RSA_WITH_SEED_CBC_SHA", 0x0096, 4162, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_SEED_CBC_SHA("TLS_DH_DSS_WITH_SEED_CBC_SHA", 0x0097, 4162, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_SEED_CBC_SHA("TLS_DH_RSA_WITH_SEED_CBC_SHA", 0x0098, 4162, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_SEED_CBC_SHA("TLS_DHE_DSS_WITH_SEED_CBC_SHA", 0x0099, 4162, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_SEED_CBC_SHA("TLS_DHE_RSA_WITH_SEED_CBC_SHA", 0x009a, 4162, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_SEED_CBC_SHA("TLS_DH_anon_WITH_SEED_CBC_SHA", 0x009b, 4162, MAX_VALUE, MAX_VALUE),
+ TLS_RSA_WITH_AES_128_GCM_SHA256("TLS_RSA_WITH_AES_128_GCM_SHA256", 0x009c, 5288, 8, 21),
+ TLS_RSA_WITH_AES_256_GCM_SHA384("TLS_RSA_WITH_AES_256_GCM_SHA384", 0x009d, 5288, 8, 21),
+ TLS_DHE_RSA_WITH_AES_128_GCM_SHA256("TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 0x009e, 5288, 8, 21),
+ TLS_DHE_RSA_WITH_AES_256_GCM_SHA384("TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 0x009f, 5288, 8, 21),
+ // TLS_DH_RSA_WITH_AES_128_GCM_SHA256("TLS_DH_RSA_WITH_AES_128_GCM_SHA256", 0x00a0, 5288, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_AES_256_GCM_SHA384("TLS_DH_RSA_WITH_AES_256_GCM_SHA384", 0x00a1, 5288, MAX_VALUE, MAX_VALUE),
+ TLS_DHE_DSS_WITH_AES_128_GCM_SHA256("TLS_DHE_DSS_WITH_AES_128_GCM_SHA256", 0x00a2, 5288, 8, 21),
+ TLS_DHE_DSS_WITH_AES_256_GCM_SHA384("TLS_DHE_DSS_WITH_AES_256_GCM_SHA384", 0x00a3, 5288, 8, 21),
+ // TLS_DH_DSS_WITH_AES_128_GCM_SHA256("TLS_DH_DSS_WITH_AES_128_GCM_SHA256", 0x00a4, 5288, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_AES_256_GCM_SHA384("TLS_DH_DSS_WITH_AES_256_GCM_SHA384", 0x00a5, 5288, MAX_VALUE, MAX_VALUE),
+ TLS_DH_anon_WITH_AES_128_GCM_SHA256("TLS_DH_anon_WITH_AES_128_GCM_SHA256", 0x00a6, 5288, 8, 21),
+ TLS_DH_anon_WITH_AES_256_GCM_SHA384("TLS_DH_anon_WITH_AES_256_GCM_SHA384", 0x00a7, 5288, 8, 21),
+ // TLS_PSK_WITH_AES_128_GCM_SHA256("TLS_PSK_WITH_AES_128_GCM_SHA256", 0x00a8, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_256_GCM_SHA384("TLS_PSK_WITH_AES_256_GCM_SHA384", 0x00a9, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_128_GCM_SHA256("TLS_DHE_PSK_WITH_AES_128_GCM_SHA256", 0x00aa, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_256_GCM_SHA384("TLS_DHE_PSK_WITH_AES_256_GCM_SHA384", 0x00ab, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_AES_128_GCM_SHA256("TLS_RSA_PSK_WITH_AES_128_GCM_SHA256", 0x00ac, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_AES_256_GCM_SHA384("TLS_RSA_PSK_WITH_AES_256_GCM_SHA384", 0x00ad, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_128_CBC_SHA256("TLS_PSK_WITH_AES_128_CBC_SHA256", 0x00ae, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_256_CBC_SHA384("TLS_PSK_WITH_AES_256_CBC_SHA384", 0x00af, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_NULL_SHA256("TLS_PSK_WITH_NULL_SHA256", 0x00b0, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_NULL_SHA384("TLS_PSK_WITH_NULL_SHA384", 0x00b1, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_128_CBC_SHA256("TLS_DHE_PSK_WITH_AES_128_CBC_SHA256", 0x00b2, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_256_CBC_SHA384("TLS_DHE_PSK_WITH_AES_256_CBC_SHA384", 0x00b3, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_NULL_SHA256("TLS_DHE_PSK_WITH_NULL_SHA256", 0x00b4, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_NULL_SHA384("TLS_DHE_PSK_WITH_NULL_SHA384", 0x00b5, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_AES_128_CBC_SHA256("TLS_RSA_PSK_WITH_AES_128_CBC_SHA256", 0x00b6, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_AES_256_CBC_SHA384("TLS_RSA_PSK_WITH_AES_256_CBC_SHA384", 0x00b7, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_NULL_SHA256("TLS_RSA_PSK_WITH_NULL_SHA256", 0x00b8, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_NULL_SHA384("TLS_RSA_PSK_WITH_NULL_SHA384", 0x00b9, 5487, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0x00ba, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256("TLS_DH_DSS_WITH_CAMELLIA_128_CBC_SHA256", 0x00bb, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_DH_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0x00bc, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256("TLS_DHE_DSS_WITH_CAMELLIA_128_CBC_SHA256", 0x00bd, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0x00be, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256("TLS_DH_anon_WITH_CAMELLIA_128_CBC_SHA256", 0x00bf, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256("TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256", 0x00c0, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256("TLS_DH_DSS_WITH_CAMELLIA_256_CBC_SHA256", 0x00c1, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256("TLS_DH_RSA_WITH_CAMELLIA_256_CBC_SHA256", 0x00c2, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256("TLS_DHE_DSS_WITH_CAMELLIA_256_CBC_SHA256", 0x00c3, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256("TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256", 0x00c4, 5932, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256("TLS_DH_anon_WITH_CAMELLIA_256_CBC_SHA256", 0x00c5, 5932, MAX_VALUE, MAX_VALUE),
+ TLS_EMPTY_RENEGOTIATION_INFO_SCSV("TLS_EMPTY_RENEGOTIATION_INFO_SCSV", 0x00ff, 5746, 6, 14),
+ TLS_ECDH_ECDSA_WITH_NULL_SHA("TLS_ECDH_ECDSA_WITH_NULL_SHA", 0xc001, 4492, 7, 14),
+ TLS_ECDH_ECDSA_WITH_RC4_128_SHA("TLS_ECDH_ECDSA_WITH_RC4_128_SHA", 0xc002, 4492, 7, 14),
+ TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA("TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA", 0xc003, 4492, 7, 14),
+ TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA", 0xc004, 4492, 7, 14),
+ TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA", 0xc005, 4492, 7, 14),
+ TLS_ECDHE_ECDSA_WITH_NULL_SHA("TLS_ECDHE_ECDSA_WITH_NULL_SHA", 0xc006, 4492, 7, 14),
+ TLS_ECDHE_ECDSA_WITH_RC4_128_SHA("TLS_ECDHE_ECDSA_WITH_RC4_128_SHA", 0xc007, 4492, 7, 14),
+ TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA("TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA", 0xc008, 4492, 7, 14),
+ TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 0xc009, 4492, 7, 14),
+ TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 0xc00a, 4492, 7, 14),
+ TLS_ECDH_RSA_WITH_NULL_SHA("TLS_ECDH_RSA_WITH_NULL_SHA", 0xc00b, 4492, 7, 14),
+ TLS_ECDH_RSA_WITH_RC4_128_SHA("TLS_ECDH_RSA_WITH_RC4_128_SHA", 0xc00c, 4492, 7, 14),
+ TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA("TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA", 0xc00d, 4492, 7, 14),
+ TLS_ECDH_RSA_WITH_AES_128_CBC_SHA("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA", 0xc00e, 4492, 7, 14),
+ TLS_ECDH_RSA_WITH_AES_256_CBC_SHA("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA", 0xc00f, 4492, 7, 14),
+ TLS_ECDHE_RSA_WITH_NULL_SHA("TLS_ECDHE_RSA_WITH_NULL_SHA", 0xc010, 4492, 7, 14),
+ TLS_ECDHE_RSA_WITH_RC4_128_SHA("TLS_ECDHE_RSA_WITH_RC4_128_SHA", 0xc011, 4492, 7, 14),
+ TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA("TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", 0xc012, 4492, 7, 14),
+ TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 0xc013, 4492, 7, 14),
+ TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 0xc014, 4492, 7, 14),
+ TLS_ECDH_anon_WITH_NULL_SHA("TLS_ECDH_anon_WITH_NULL_SHA", 0xc015, 4492, 7, 14),
+ TLS_ECDH_anon_WITH_RC4_128_SHA("TLS_ECDH_anon_WITH_RC4_128_SHA", 0xc016, 4492, 7, 14),
+ TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA("TLS_ECDH_anon_WITH_3DES_EDE_CBC_SHA", 0xc017, 4492, 7, 14),
+ TLS_ECDH_anon_WITH_AES_128_CBC_SHA("TLS_ECDH_anon_WITH_AES_128_CBC_SHA", 0xc018, 4492, 7, 14),
+ TLS_ECDH_anon_WITH_AES_256_CBC_SHA("TLS_ECDH_anon_WITH_AES_256_CBC_SHA", 0xc019, 4492, 7, 14),
+ // TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA("TLS_SRP_SHA_WITH_3DES_EDE_CBC_SHA", 0xc01a, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA("TLS_SRP_SHA_RSA_WITH_3DES_EDE_CBC_SHA", 0xc01b, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA("TLS_SRP_SHA_DSS_WITH_3DES_EDE_CBC_SHA", 0xc01c, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_WITH_AES_128_CBC_SHA("TLS_SRP_SHA_WITH_AES_128_CBC_SHA", 0xc01d, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA("TLS_SRP_SHA_RSA_WITH_AES_128_CBC_SHA", 0xc01e, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA("TLS_SRP_SHA_DSS_WITH_AES_128_CBC_SHA", 0xc01f, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_WITH_AES_256_CBC_SHA("TLS_SRP_SHA_WITH_AES_256_CBC_SHA", 0xc020, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA("TLS_SRP_SHA_RSA_WITH_AES_256_CBC_SHA", 0xc021, 5054, MAX_VALUE, MAX_VALUE),
+ // TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA("TLS_SRP_SHA_DSS_WITH_AES_256_CBC_SHA", 0xc022, 5054, MAX_VALUE, MAX_VALUE),
+ TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256("TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 0xc023, 5289, 7, 21),
+ TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384("TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", 0xc024, 5289, 7, 21),
+ TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256("TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256", 0xc025, 5289, 7, 21),
+ TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384("TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384", 0xc026, 5289, 7, 21),
+ TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256("TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 0xc027, 5289, 7, 21),
+ TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384("TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", 0xc028, 5289, 7, 21),
+ TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256("TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256", 0xc029, 5289, 7, 21),
+ TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384("TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384", 0xc02a, 5289, 7, 21),
+ TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 0xc02b, 5289, 8, 21),
+ TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 0xc02c, 5289, 8, 21),
+ TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256("TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256", 0xc02d, 5289, 8, 21),
+ TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384("TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384", 0xc02e, 5289, 8, 21),
+ TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 0xc02f, 5289, 8, 21),
+ TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384("TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 0xc030, 5289, 8, 21),
+ TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256("TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256", 0xc031, 5289, 8, 21),
+ TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384("TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384", 0xc032, 5289, 8, 21),
+ // TLS_ECDHE_PSK_WITH_RC4_128_SHA("TLS_ECDHE_PSK_WITH_RC4_128_SHA", 0xc033, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA("TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA", 0xc034, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA("TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA", 0xc035, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA("TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA", 0xc036, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256("TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256", 0xc037, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384("TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384", 0xc038, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_NULL_SHA("TLS_ECDHE_PSK_WITH_NULL_SHA", 0xc039, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_NULL_SHA256("TLS_ECDHE_PSK_WITH_NULL_SHA256", 0xc03a, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_NULL_SHA384("TLS_ECDHE_PSK_WITH_NULL_SHA384", 0xc03b, 5489, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_ARIA_128_CBC_SHA256("TLS_RSA_WITH_ARIA_128_CBC_SHA256", 0xc03c, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_ARIA_256_CBC_SHA384("TLS_RSA_WITH_ARIA_256_CBC_SHA384", 0xc03d, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256("TLS_DH_DSS_WITH_ARIA_128_CBC_SHA256", 0xc03e, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384("TLS_DH_DSS_WITH_ARIA_256_CBC_SHA384", 0xc03f, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256("TLS_DH_RSA_WITH_ARIA_128_CBC_SHA256", 0xc040, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384("TLS_DH_RSA_WITH_ARIA_256_CBC_SHA384", 0xc041, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256("TLS_DHE_DSS_WITH_ARIA_128_CBC_SHA256", 0xc042, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384("TLS_DHE_DSS_WITH_ARIA_256_CBC_SHA384", 0xc043, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256("TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256", 0xc044, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384("TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384", 0xc045, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_ARIA_128_CBC_SHA256("TLS_DH_anon_WITH_ARIA_128_CBC_SHA256", 0xc046, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_ARIA_256_CBC_SHA384("TLS_DH_anon_WITH_ARIA_256_CBC_SHA384", 0xc047, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256("TLS_ECDHE_ECDSA_WITH_ARIA_128_CBC_SHA256", 0xc048, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384("TLS_ECDHE_ECDSA_WITH_ARIA_256_CBC_SHA384", 0xc049, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256("TLS_ECDH_ECDSA_WITH_ARIA_128_CBC_SHA256", 0xc04a, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384("TLS_ECDH_ECDSA_WITH_ARIA_256_CBC_SHA384", 0xc04b, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256("TLS_ECDHE_RSA_WITH_ARIA_128_CBC_SHA256", 0xc04c, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384("TLS_ECDHE_RSA_WITH_ARIA_256_CBC_SHA384", 0xc04d, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256("TLS_ECDH_RSA_WITH_ARIA_128_CBC_SHA256", 0xc04e, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384("TLS_ECDH_RSA_WITH_ARIA_256_CBC_SHA384", 0xc04f, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_ARIA_128_GCM_SHA256("TLS_RSA_WITH_ARIA_128_GCM_SHA256", 0xc050, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_ARIA_256_GCM_SHA384("TLS_RSA_WITH_ARIA_256_GCM_SHA384", 0xc051, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256("TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256", 0xc052, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384("TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384", 0xc053, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256("TLS_DH_RSA_WITH_ARIA_128_GCM_SHA256", 0xc054, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384("TLS_DH_RSA_WITH_ARIA_256_GCM_SHA384", 0xc055, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256("TLS_DHE_DSS_WITH_ARIA_128_GCM_SHA256", 0xc056, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384("TLS_DHE_DSS_WITH_ARIA_256_GCM_SHA384", 0xc057, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256("TLS_DH_DSS_WITH_ARIA_128_GCM_SHA256", 0xc058, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384("TLS_DH_DSS_WITH_ARIA_256_GCM_SHA384", 0xc059, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_ARIA_128_GCM_SHA256("TLS_DH_anon_WITH_ARIA_128_GCM_SHA256", 0xc05a, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_ARIA_256_GCM_SHA384("TLS_DH_anon_WITH_ARIA_256_GCM_SHA384", 0xc05b, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256("TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256", 0xc05c, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384("TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384", 0xc05d, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256("TLS_ECDH_ECDSA_WITH_ARIA_128_GCM_SHA256", 0xc05e, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384("TLS_ECDH_ECDSA_WITH_ARIA_256_GCM_SHA384", 0xc05f, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256("TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256", 0xc060, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384("TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384", 0xc061, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256("TLS_ECDH_RSA_WITH_ARIA_128_GCM_SHA256", 0xc062, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384("TLS_ECDH_RSA_WITH_ARIA_256_GCM_SHA384", 0xc063, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_ARIA_128_CBC_SHA256("TLS_PSK_WITH_ARIA_128_CBC_SHA256", 0xc064, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_ARIA_256_CBC_SHA384("TLS_PSK_WITH_ARIA_256_CBC_SHA384", 0xc065, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256("TLS_DHE_PSK_WITH_ARIA_128_CBC_SHA256", 0xc066, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384("TLS_DHE_PSK_WITH_ARIA_256_CBC_SHA384", 0xc067, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256("TLS_RSA_PSK_WITH_ARIA_128_CBC_SHA256", 0xc068, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384("TLS_RSA_PSK_WITH_ARIA_256_CBC_SHA384", 0xc069, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_ARIA_128_GCM_SHA256("TLS_PSK_WITH_ARIA_128_GCM_SHA256", 0xc06a, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_ARIA_256_GCM_SHA384("TLS_PSK_WITH_ARIA_256_GCM_SHA384", 0xc06b, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256("TLS_DHE_PSK_WITH_ARIA_128_GCM_SHA256", 0xc06c, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384("TLS_DHE_PSK_WITH_ARIA_256_GCM_SHA384", 0xc06d, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256("TLS_RSA_PSK_WITH_ARIA_128_GCM_SHA256", 0xc06e, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384("TLS_RSA_PSK_WITH_ARIA_256_GCM_SHA384", 0xc06f, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256("TLS_ECDHE_PSK_WITH_ARIA_128_CBC_SHA256", 0xc070, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384("TLS_ECDHE_PSK_WITH_ARIA_256_CBC_SHA384", 0xc071, 6209, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc072, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384("TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc073, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc074, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384("TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc075, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc076, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384("TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc077, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256("TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256", 0xc078, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384("TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384", 0xc079, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc07a, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc07b, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc07c, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc07d, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_DH_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc07e, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_DH_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc07f, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256("TLS_DHE_DSS_WITH_CAMELLIA_128_GCM_SHA256", 0xc080, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384("TLS_DHE_DSS_WITH_CAMELLIA_256_GCM_SHA384", 0xc081, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256("TLS_DH_DSS_WITH_CAMELLIA_128_GCM_SHA256", 0xc082, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384("TLS_DH_DSS_WITH_CAMELLIA_256_GCM_SHA384", 0xc083, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256("TLS_DH_anon_WITH_CAMELLIA_128_GCM_SHA256", 0xc084, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384("TLS_DH_anon_WITH_CAMELLIA_256_GCM_SHA384", 0xc085, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc086, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc087, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc088, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc089, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc08a, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc08b, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256("TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256", 0xc08c, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384("TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384", 0xc08d, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256("TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256", 0xc08e, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384("TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384", 0xc08f, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256("TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256", 0xc090, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384("TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384", 0xc091, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256("TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256", 0xc092, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384("TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384", 0xc093, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256("TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc094, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384("TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc095, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256("TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc096, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384("TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc097, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256("TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc098, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384("TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc099, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256("TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256", 0xc09a, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384("TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384", 0xc09b, 6367, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_AES_128_CCM("TLS_RSA_WITH_AES_128_CCM", 0xc09c, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_AES_256_CCM("TLS_RSA_WITH_AES_256_CCM", 0xc09d, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_AES_128_CCM("TLS_DHE_RSA_WITH_AES_128_CCM", 0xc09e, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_AES_256_CCM("TLS_DHE_RSA_WITH_AES_256_CCM", 0xc09f, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_AES_128_CCM_8("TLS_RSA_WITH_AES_128_CCM_8", 0xc0a0, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_RSA_WITH_AES_256_CCM_8("TLS_RSA_WITH_AES_256_CCM_8", 0xc0a1, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_AES_128_CCM_8("TLS_DHE_RSA_WITH_AES_128_CCM_8", 0xc0a2, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_RSA_WITH_AES_256_CCM_8("TLS_DHE_RSA_WITH_AES_256_CCM_8", 0xc0a3, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_128_CCM("TLS_PSK_WITH_AES_128_CCM", 0xc0a4, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_256_CCM("TLS_PSK_WITH_AES_256_CCM", 0xc0a5, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_128_CCM("TLS_DHE_PSK_WITH_AES_128_CCM", 0xc0a6, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_DHE_PSK_WITH_AES_256_CCM("TLS_DHE_PSK_WITH_AES_256_CCM", 0xc0a7, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_128_CCM_8("TLS_PSK_WITH_AES_128_CCM_8", 0xc0a8, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_WITH_AES_256_CCM_8("TLS_PSK_WITH_AES_256_CCM_8", 0xc0a9, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_DHE_WITH_AES_128_CCM_8("TLS_PSK_DHE_WITH_AES_128_CCM_8", 0xc0aa, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_PSK_DHE_WITH_AES_256_CCM_8("TLS_PSK_DHE_WITH_AES_256_CCM_8", 0xc0ab, 6655, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_AES_128_CCM("TLS_ECDHE_ECDSA_WITH_AES_128_CCM", 0xc0ac, 7251, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_AES_256_CCM("TLS_ECDHE_ECDSA_WITH_AES_256_CCM", 0xc0ad, 7251, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8("TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8", 0xc0ae, 7251, MAX_VALUE, MAX_VALUE),
+ // TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8("TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8", 0xc0af, 7251, MAX_VALUE, MAX_VALUE),
+ ;
+
+ final String javaName;
+
+ /**
+ * @param javaName the name used by Java APIs for this cipher suite. Different than the IANA name
+ * for older cipher suites because the prefix is {@code SSL_} instead of {@code TLS_}.
+ * @param value the integer identifier for this cipher suite. (Documentation only.)
+ * @param rfc the RFC describing this cipher suite. (Documentation only.)
+ * @param sinceJavaVersion the first major Java release supporting this cipher suite.
+ * @param sinceAndroidVersion the first Android SDK version supporting this cipher suite.
+ */
+ private CipherSuite(
+ String javaName, int value, int rfc, int sinceJavaVersion, int sinceAndroidVersion) {
+ this.javaName = javaName;
+ }
+
+ public static CipherSuite forJavaName(String javaName) {
+ return javaName.startsWith("SSL_")
+ ? valueOf("TLS_" + javaName.substring(4))
+ : valueOf(javaName);
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index d0cd18b..8d8586e 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -18,25 +18,25 @@
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.HttpAuthenticator;
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.SpdyTransport;
-import com.squareup.okhttp.internal.TlsConfiguration;
-import com.squareup.okhttp.internal.TlsFallbackStrategy;
+import com.squareup.okhttp.internal.http.Transport;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
-import java.io.Closeable;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import java.io.IOException;
import java.net.Proxy;
import java.net.Socket;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.util.concurrent.TimeUnit;
import javax.net.ssl.SSLSocket;
-import okio.ByteString;
-import okio.OkBuffer;
import okio.Source;
-import static com.squareup.okhttp.internal.Util.closeQuietly;
+import static com.squareup.okhttp.internal.Util.getDefaultPort;
+import static com.squareup.okhttp.internal.Util.getEffectivePort;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
@@ -47,7 +47,7 @@
*
* <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 {@link ConnectionPool connection pool}.
+ * 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.
@@ -58,15 +58,15 @@
* <ul>
* <li>Server Name Indication (SNI) enables one IP address to negotiate secure
* connections for multiple domain names.
- * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used
- * for both HTTP and SPDY protocols.
+ * <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.
*/
-public final class Connection implements Closeable {
+public final class Connection {
private final ConnectionPool pool;
private final Route route;
@@ -74,8 +74,7 @@
private boolean connected = false;
private HttpConnection httpConnection;
private SpdyConnection spdyConnection;
- private TlsConfiguration tlsConfiguration;
- private int httpMinorVersion = 1; // Assume HTTP/1.1
+ private Protocol protocol = Protocol.HTTP_1_1;
private long idleStartTimeNs;
private Handshake handshake;
private int recycleCount;
@@ -92,13 +91,13 @@
this.route = route;
}
- public Object getOwner() {
+ Object getOwner() {
synchronized (pool) {
return owner;
}
}
- public void setOwner(Object owner) {
+ void setOwner(Object owner) {
if (isSpdy()) return; // SPDY connections are shared.
synchronized (pool) {
if (this.owner != null) throw new IllegalStateException("Connection already has an owner!");
@@ -112,7 +111,7 @@
* false if the connection cannot be pooled or reused, such as if it was
* closed with {@link #closeIfOwnedBy}.
*/
- public boolean clearOwner() {
+ boolean clearOwner() {
synchronized (pool) {
if (owner == null) {
// No owner? Don't reuse this connection.
@@ -128,7 +127,7 @@
* 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.
*/
- public void closeIfOwnedBy(Object owner) throws IOException {
+ void closeIfOwnedBy(Object owner) throws IOException {
if (isSpdy()) throw new IllegalStateException();
synchronized (pool) {
if (this.owner != owner) {
@@ -142,132 +141,148 @@
socket.close();
}
- public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest)
+ void connect(int connectTimeout, int readTimeout, int writeTimeout, Request tunnelRequest)
throws IOException {
if (connected) throw new IllegalStateException("already connected");
- TlsFallbackStrategy tlsFallbackStrategy = null;
+ if (route.proxy.type() == Proxy.Type.DIRECT || route.proxy.type() == Proxy.Type.HTTP) {
+ socket = route.address.socketFactory.createSocket();
+ } else {
+ socket = new Socket(route.proxy);
+ }
+
+ socket.setSoTimeout(readTimeout);
+ Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
+
if (route.address.sslSocketFactory != null) {
- tlsFallbackStrategy = TlsFallbackStrategy.create();
+ upgradeToTls(tunnelRequest, readTimeout, writeTimeout);
+ } else {
+ httpConnection = new HttpConnection(pool, this, socket);
}
-
- while (!connected) {
- if (route.proxy.type() == Proxy.Type.DIRECT || route.proxy.type() == Proxy.Type.HTTP) {
- socket = route.address.socketFactory.createSocket();
- } else {
- socket = new Socket(route.proxy);
- }
-
- socket.setSoTimeout(readTimeout);
- Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
-
- if (tlsFallbackStrategy != null) {
- boolean success = upgradeToTls(tlsFallbackStrategy, tunnelRequest);
- if (!success) {
- continue;
- }
- } else {
- httpConnection = new HttpConnection(pool, this, socket);
- }
- connected = true;
- }
+ connected = true;
}
/**
- * Create an {@code SSLSocket} and perform the TLS handshake and certificate validation.
- *
- * Returns {@code true} if the connection was successful, {@code false} if the connection was
- * unsuccessful but should be retried and throws an {@link IOException} if the connection failed
- * in a non-retryable fashion.
+ * Connects this connection if it isn't already. This creates tunnels, shares
+ * the connection with the connection pool, and configures timeouts.
*/
- private boolean upgradeToTls(TlsFallbackStrategy tlsFallbackStrategy, TunnelRequest tunnelRequest)
- throws IOException {
+ void connectAndSetOwner(OkHttpClient client, Object owner, Request request) throws IOException {
+ setOwner(owner);
+ if (!isConnected()) {
+ Request tunnelRequest = tunnelRequest(request);
+ connect(client.getConnectTimeout(), client.getReadTimeout(),
+ client.getWriteTimeout(), tunnelRequest);
+ if (isSpdy()) {
+ client.getConnectionPool().share(this);
+ }
+ client.routeDatabase().connected(getRoute());
+ }
+
+ setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
+ }
+
+ /**
+ * 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 tunnelRequest(Request request) throws IOException {
+ if (!route.requiresTunnel()) return null;
+
+ String host = request.url().getHost();
+ int port = getEffectivePort(request.url());
+ String authority = (port == getDefaultPort("https")) ? host : (host + ":" + port);
+ Request.Builder result = new Request.Builder()
+ .url(new URL("https", host, port, "/"))
+ .header("Host", authority)
+ .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();
+ }
+
+ /**
+ * Create an {@code SSLSocket} and perform the TLS handshake and certificate
+ * validation.
+ */
+ private void upgradeToTls(Request tunnelRequest, int readTimeout, int writeTimeout)
+ throws IOException {
Platform platform = Platform.get();
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
- if (requiresTunnel()) {
- makeTunnel(tunnelRequest);
+ if (tunnelRequest != null) {
+ makeTunnel(tunnelRequest, readTimeout, writeTimeout);
}
+ // Create the wrapper over connected socket.
+ socket = route.address.sslSocketFactory
+ .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
+ SSLSocket sslSocket = (SSLSocket) socket;
+
+ // Configure the socket's ciphers, TLS versions, and extensions.
+ route.connectionSpec.apply(sslSocket, route);
+
try {
- // Create the wrapper over connected socket.
- socket = route.address.sslSocketFactory
- .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
- SSLSocket sslSocket = (SSLSocket) socket;
-
- TlsConfiguration tlsConfiguration =
- tlsFallbackStrategy.configureSecureSocket(sslSocket, route.address.uriHost, platform);
- boolean useNpn = tlsConfiguration.supportsNpn();
- if (useNpn) {
- boolean http2 = route.address.protocols.contains(Protocol.HTTP_2);
- boolean spdy3 = route.address.protocols.contains(Protocol.SPDY_3);
- if (http2 && spdy3) {
- platform.setNpnProtocols(sslSocket, Protocol.HTTP2_SPDY3_AND_HTTP);
- } else if (http2) {
- platform.setNpnProtocols(sslSocket, Protocol.HTTP2_AND_HTTP_11);
- } else if (spdy3) {
- platform.setNpnProtocols(sslSocket, Protocol.SPDY3_AND_HTTP11);
- }
- }
-
// Force handshake. This can throw!
sslSocket.startHandshake();
- // Verify that the socket's certificates are acceptable for the target host.
- if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
- throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
+ String maybeProtocol;
+ if (route.connectionSpec.supportsTlsExtensions()
+ && (maybeProtocol = platform.getSelectedProtocol(sslSocket)) != null) {
+ protocol = Protocol.get(maybeProtocol); // Throws IOE on unknown.
}
-
- handshake = Handshake.get(sslSocket.getSession());
-
- ByteString maybeProtocol;
- Protocol selectedProtocol = Protocol.HTTP_11;
- if (useNpn && (maybeProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
- selectedProtocol = Protocol.find(maybeProtocol); // Throws IOE on unknown.
- }
-
- if (selectedProtocol.spdyVariant) {
- sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
- spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
- .protocol(selectedProtocol).build();
- spdyConnection.sendConnectionHeader();
- } else {
- httpConnection = new HttpConnection(pool, this, socket);
- }
- this.tlsConfiguration = tlsConfiguration;
- } catch (IOException e){
- boolean retryConnect = tlsFallbackStrategy.connectionFailed(e);
- if (retryConnect) {
- closeQuietly(socket);
- handshake = null;
- socket = null;
- return false;
- }
- throw e;
+ } finally {
+ platform.afterHandshake(sslSocket);
}
- return true;
+ handshake = Handshake.get(sslSocket.getSession());
+
+ // Verify that the socket's certificates are acceptable for the target host.
+ if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
+ X509Certificate cert = (X509Certificate) sslSocket.getSession().getPeerCertificates()[0];
+ throw new IOException("Hostname " + route.address.uriHost + " 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.
+ route.address.certificatePinner.check(route.address.uriHost, handshake.peerCertificates());
+
+ if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
+ sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
+ spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
+ .protocol(protocol).build();
+ spdyConnection.sendConnectionPreface();
+ } else {
+ httpConnection = new HttpConnection(pool, this, socket);
+ }
}
/** Returns true if {@link #connect} has been attempted on this connection. */
- public boolean isConnected() {
+ boolean isConnected() {
return connected;
}
- @Override public void close() throws IOException {
- if (socket != null) socket.close();
- }
-
/** Returns the route used by this connection. */
public Route getRoute() {
return route;
}
- public TlsConfiguration getTlsConfiguration() {
- return tlsConfiguration;
- }
-
/**
* Returns the socket that this connection uses, or null if the connection
* is not currently connected.
@@ -277,7 +292,7 @@
}
/** Returns true if this connection is alive. */
- public boolean isAlive() {
+ boolean isAlive() {
return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
}
@@ -286,34 +301,26 @@
* connection. This is more expensive and more accurate than {@link
* #isAlive()}; callers should check {@link #isAlive()} first.
*/
- public boolean isReadable() {
+ boolean isReadable() {
if (httpConnection != null) return httpConnection.isReadable();
return true; // SPDY connections, and connections before connect() are both optimistic.
}
- public void resetIdleStartTime() {
+ void resetIdleStartTime() {
if (spdyConnection != null) throw new IllegalStateException("spdyConnection != null");
this.idleStartTimeNs = System.nanoTime();
}
/** Returns true if this connection is idle. */
- public boolean isIdle() {
+ boolean isIdle() {
return spdyConnection == null || spdyConnection.isIdle();
}
/**
- * Returns true if this connection has been idle for longer than
- * {@code keepAliveDurationNs}.
- */
- public boolean isExpired(long keepAliveDurationNs) {
- return getIdleStartTimeNs() < System.nanoTime() - keepAliveDurationNs;
- }
-
- /**
* Returns the time in ns when this connection became idle. Undefined if
* this connection is not idle.
*/
- public long getIdleStartTimeNs() {
+ long getIdleStartTimeNs() {
return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs();
}
@@ -322,7 +329,7 @@
}
/** Returns the transport appropriate for this connection. */
- public Object newTransport(HttpEngine httpEngine) throws IOException {
+ Transport newTransport(HttpEngine httpEngine) throws IOException {
return (spdyConnection != null)
? new SpdyTransport(httpEngine, spdyConnection)
: new HttpTransport(httpEngine, httpConnection);
@@ -332,38 +339,38 @@
* Returns true if this is a SPDY connection. Such connections can be used
* in multiple HTTP requests simultaneously.
*/
- public boolean isSpdy() {
+ boolean isSpdy() {
return spdyConnection != null;
}
/**
- * Returns the minor HTTP version that should be used for future requests on
- * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default
- * value is 1 for new connections.
+ * Returns the protocol negotiated by this connection, or {@link
+ * Protocol#HTTP_1_1} if no protocol has been negotiated.
*/
- public int getHttpMinorVersion() {
- return httpMinorVersion;
- }
-
- public void setHttpMinorVersion(int httpMinorVersion) {
- this.httpMinorVersion = httpMinorVersion;
+ public Protocol getProtocol() {
+ return protocol;
}
/**
- * Returns true if the HTTP connection needs to tunnel one protocol over
- * another, such as when using HTTPS through an HTTP proxy. When doing so,
- * we must avoid buffering bytes intended for the higher-level protocol.
+ * 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.
*/
- public boolean requiresTunnel() {
- return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP;
+ void setProtocol(Protocol protocol) {
+ if (protocol == null) throw new IllegalArgumentException("protocol == null");
+ this.protocol = protocol;
}
- public void updateReadTimeout(int newTimeout) throws IOException {
- if (!connected) throw new IllegalStateException("updateReadTimeout - not connected");
- socket.setSoTimeout(newTimeout);
+ void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) throws IOException {
+ if (!connected) throw new IllegalStateException("setTimeouts - not connected");
+
+ // Don't set timeouts on shared SPDY connections.
+ if (httpConnection != null) {
+ socket.setSoTimeout(readTimeoutMillis);
+ httpConnection.setTimeouts(readTimeoutMillis, writeTimeoutMillis);
+ }
}
- public void incrementRecycleCount() {
+ void incrementRecycleCount() {
recycleCount++;
}
@@ -371,7 +378,7 @@
* Returns the number of times this connection has been returned to the
* connection pool.
*/
- public int recycleCount() {
+ int recycleCount() {
return recycleCount;
}
@@ -380,10 +387,12 @@
* CONNECT request to create the proxy connection. This may need to be
* retried if the proxy requires authorization.
*/
- private void makeTunnel(TunnelRequest tunnelRequest) throws IOException {
+ private void makeTunnel(Request request, int readTimeout, int writeTimeout)
+ throws IOException {
HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
- Request request = tunnelRequest.getRequest();
- String requestLine = tunnelRequest.requestLine();
+ tunnelConnection.setTimeouts(readTimeout, writeTimeout);
+ URL url = request.url();
+ String requestLine = "CONNECT " + url.getHost() + ":" + url.getPort() + " HTTP/1.1";
while (true) {
tunnelConnection.writeRequest(request.headers(), requestLine);
tunnelConnection.flush();
@@ -391,12 +400,12 @@
// 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 != -1) {
- Source body = tunnelConnection.newFixedLengthSource(null, contentLength);
- Util.skipAll(body, Integer.MAX_VALUE);
- } else {
- tunnelConnection.emptyResponseBody();
+ 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:
@@ -410,7 +419,7 @@
return;
case HTTP_PROXY_AUTH:
- request = HttpAuthenticator.processAuthHeader(
+ request = OkHeaders.processAuthHeader(
route.address.authenticator, response, route.proxy);
if (request != null) continue;
throw new IOException("Failed to authenticate with proxy");
@@ -421,4 +430,18 @@
}
}
}
+
+ @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
+ + '}';
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index 1840701..ba664ea 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -23,7 +23,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
-import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@@ -51,8 +51,7 @@
* parameters do so before making HTTP connections, and that this class is
* initialized lazily.
*/
-public class ConnectionPool {
- private static final int MAX_CONNECTIONS_TO_CLEANUP = 2;
+public final class ConnectionPool {
private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
private static final ConnectionPool systemDefault;
@@ -76,98 +75,26 @@
private final int maxIdleConnections;
private final long keepAliveDurationNs;
- private final LinkedList<Connection> connections = new LinkedList<Connection>();
+ private final LinkedList<Connection> connections = new LinkedList<>();
- /** We use a single background thread to cleanup expired connections. */
- private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
- 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
- Util.threadFactory("OkHttp ConnectionPool", true));
-
- private enum CleanMode {
- /**
- * Connection clean up is driven by usage of the pool. Each usage of the pool can schedule a
- * clean up. A pool left in this state and unused may contain idle connections indefinitely.
- */
- NORMAL,
- /**
- * Entered when a pool has been orphaned and is not expected to receive more usage, except for
- * references held by existing connections. See {@link #enterDrainMode()}.
- * A thread runs periodically to close idle connections in the pool until the pool is empty and
- * then the state moves to {@link #DRAINED}.
- */
- DRAINING,
- /**
- * The pool is empty and no clean-up is taking place. Connections may still be added to the
- * pool due to latent references to the pool, in which case the pool re-enters
- * {@link #DRAINING}. If the pool is DRAINED and no longer referenced it is safe to be garbage
- * collected.
- */
- DRAINED
- }
- /** The current mode for cleaning connections in the pool */
- private CleanMode cleanMode = CleanMode.NORMAL;
-
- // A scheduled drainModeRunnable keeps a reference to the enclosing ConnectionPool,
- // preventing the ConnectionPool from being garbage collected before all held connections have
- // been explicitly closed. If this was not the case any open connections in the pool would trigger
- // StrictMode violations in Android when they were garbage collected. http://b/18369687
- private final Runnable drainModeRunnable = new Runnable() {
- @Override public void run() {
- // Close any connections we can.
- connectionsCleanupRunnable.run();
-
- synchronized (ConnectionPool.this) {
- // See whether we should continue checking the connection pool.
- if (connections.size() > 0) {
- // Pause to avoid checking too regularly, which would drain the battery on mobile
- // devices. The wait() surrenders the pool monitor and will not block other calls.
- try {
- // Use the keep alive duration as a rough indicator of a good check interval.
- long keepAliveDurationMillis = keepAliveDurationNs / (1000 * 1000);
- ConnectionPool.this.wait(keepAliveDurationMillis);
- } catch (InterruptedException e) {
- // Ignored.
- }
-
- // Reschedule "this" to perform another clean-up.
- executorService.execute(this);
- } else {
- cleanMode = CleanMode.DRAINED;
- }
- }
- }
- };
+ /**
+ * 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.
+ */
+ private 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() {
@Override public void run() {
- List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP);
- int idleConnectionCount = 0;
- synchronized (ConnectionPool.this) {
- for (ListIterator<Connection> i = connections.listIterator(connections.size());
- i.hasPrevious(); ) {
- Connection connection = i.previous();
- if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) {
- i.remove();
- expiredConnections.add(connection);
- if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break;
- } else if (connection.isIdle()) {
- idleConnectionCount++;
- }
- }
-
- for (ListIterator<Connection> i = connections.listIterator(connections.size());
- i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) {
- Connection connection = i.previous();
- if (connection.isIdle()) {
- expiredConnections.add(connection);
- i.remove();
- --idleConnectionCount;
- }
- }
- }
- for (Connection expiredConnection : expiredConnections) {
- Util.closeQuietly(expiredConnection);
- }
+ runCleanupUntilPoolIsEmpty();
}
};
@@ -176,33 +103,6 @@
this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
}
- /**
- * Returns a snapshot of the connections in this pool, ordered from newest to
- * oldest. Waits for the cleanup callable to run if it is currently scheduled.
- * Only use in tests.
- */
- List<Connection> getConnections() {
- waitForCleanupCallableToRun();
- synchronized (this) {
- return new ArrayList<Connection>(connections);
- }
- }
-
- /**
- * Blocks until the executor service has processed all currently enqueued
- * jobs.
- */
- private void waitForCleanupCallableToRun() {
- try {
- executorService.submit(new Runnable() {
- @Override public void run() {
- }
- }).get();
- } catch (Exception e) {
- throw new AssertionError();
- }
- }
-
public static ConnectionPool getDefault() {
return systemDefault;
}
@@ -212,8 +112,14 @@
return connections.size();
}
- /** Returns total number of spdy connections in the pool. */
+ /** @deprecated Use {@link #getMultiplexedConnectionCount()}. */
+ @Deprecated
public synchronized int getSpdyConnectionCount() {
+ return getMultiplexedConnectionCount();
+ }
+
+ /** Returns total number of multiplexed connections in the pool. */
+ public synchronized int getMultiplexedConnectionCount() {
int total = 0;
for (Connection connection : connections) {
if (connection.isSpdy()) total++;
@@ -223,11 +129,7 @@
/** Returns total number of http connections in the pool. */
public synchronized int getHttpConnectionCount() {
- int total = 0;
- for (Connection connection : connections) {
- if (!connection.isSpdy()) total++;
- }
- return total;
+ return connections.size() - getMultiplexedConnectionCount();
}
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
@@ -246,7 +148,7 @@
try {
Platform.get().tagSocket(connection.getSocket());
} catch (SocketException e) {
- Util.closeQuietly(connection);
+ Util.closeQuietly(connection.getSocket());
// When unable to tag, skip recycling and close
Platform.get().logW("Unable to tagSocket(): " + e);
continue;
@@ -260,7 +162,6 @@
connections.addFirst(foundConnection); // Add it back after iteration.
}
- scheduleCleanupAsRequired();
return foundConnection;
}
@@ -270,7 +171,7 @@
*
* <p>It is an error to use {@code connection} after calling this method.
*/
- public void recycle(Connection connection) {
+ void recycle(Connection connection) {
if (connection.isSpdy()) {
return;
}
@@ -280,7 +181,7 @@
}
if (!connection.isAlive()) {
- Util.closeQuietly(connection);
+ Util.closeQuietly(connection.getSocket());
return;
}
@@ -289,78 +190,148 @@
} catch (SocketException e) {
// When unable to remove tagging, skip recycling and close.
Platform.get().logW("Unable to untagSocket(): " + e);
- Util.closeQuietly(connection);
+ Util.closeQuietly(connection.getSocket());
return;
}
synchronized (this) {
- connections.addFirst(connection);
+ addConnection(connection);
connection.incrementRecycleCount();
connection.resetIdleStartTime();
- scheduleCleanupAsRequired();
}
+ }
+ 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}.
*/
- public void share(Connection connection) {
+ void share(Connection connection) {
if (!connection.isSpdy()) throw new IllegalArgumentException();
- if (connection.isAlive()) {
- synchronized (this) {
- connections.addFirst(connection);
- scheduleCleanupAsRequired();
- }
+ if (!connection.isAlive()) return;
+ synchronized (this) {
+ addConnection(connection);
}
}
/** Close and remove all connections in the pool. */
public void evictAll() {
- List<Connection> connections;
+ List<Connection> toEvict;
synchronized (this) {
- connections = new ArrayList<Connection>(this.connections);
- this.connections.clear();
+ toEvict = new ArrayList<>(connections);
+ connections.clear();
+ notifyAll();
}
- for (int i = 0, size = connections.size(); i < size; i++) {
- Util.closeQuietly(connections.get(i));
+ 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.
}
}
/**
- * A less abrupt way of draining the pool than {@link #evictAll()}. For use when the pool
- * may still be referenced by active shared connections which cannot safely be closed.
+ * 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.
*/
- public void enterDrainMode() {
- synchronized(this) {
- cleanMode = CleanMode.DRAINING;
- executorService.execute(drainModeRunnable);
+ // 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 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 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) {
+ }
+ }
}
+
+ // 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());
+ }
+
+ return true; // Cleanup continues.
}
- public boolean isDrained() {
- synchronized(this) {
- return cleanMode == CleanMode.DRAINED;
- }
+ /**
+ * Replace the default {@link Executor} with a different one. Only use in tests.
+ */
+ // VisibleForTesting
+ void replaceCleanupExecutorForTests(Executor cleanupExecutor) {
+ this.executor = cleanupExecutor;
}
- // Callers must synchronize on "this".
- private void scheduleCleanupAsRequired() {
- switch (cleanMode) {
- case NORMAL:
- executorService.execute(connectionsCleanupRunnable);
- break;
- case DRAINING:
- // Do nothing -drainModeRunnable is already scheduled, and will reschedules itself as
- // needed.
- break;
- case DRAINED:
- // A new connection has potentially been offered up to a drained pool. Restart the drain.
- cleanMode = CleanMode.DRAINING;
- executorService.execute(drainModeRunnable);
- break;
- }
+ /**
+ * 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);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
new file mode 100644
index 0000000..e905052
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2014 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.Platform;
+import com.squareup.okhttp.internal.Util;
+import java.util.Arrays;
+import java.util.List;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
+ * https:} URLs, this includes the TLS version and ciphers to use when negotiating a secure
+ * connection.
+ */
+public final class ConnectionSpec {
+
+ /** A modern TLS connection with extensions like SNI and ALPN available. */
+ public static final ConnectionSpec MODERN_TLS = new Builder(true)
+ .cipherSuites(
+ // This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5.
+ // All of these suites are available on Android L; earlier releases support a subset of
+ // these suites. https://github.com/square/okhttp/issues/330
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+ CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+ CipherSuite.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+ CipherSuite.TLS_ECDHE_RSA_WITH_RC4_128_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,
+ CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
+ CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA,
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5
+ )
+ .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
+ .supportsTlsExtensions(true)
+ .build();
+
+ /** A backwards-compatible fallback connection for interop with obsolete servers. */
+ public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_0)
+ .supportsTlsExtensions(true)
+ .build();
+
+ /** 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 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;
+ this.tlsVersions = builder.tlsVersions;
+ this.supportsTlsExtensions = builder.supportsTlsExtensions;
+ }
+
+ public boolean isTls() {
+ return tls;
+ }
+
+ /**
+ * Return the cipher suites to use with the connection. This method can return {@code null} if the
+ * ciphers enabled by default should be used.
+ */
+ public List<CipherSuite> cipherSuites() {
+ 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]);
+ }
+ return Util.immutableList(result);
+ }
+
+ public List<TlsVersion> tlsVersions() {
+ TlsVersion[] result = new TlsVersion[tlsVersions.length];
+ for (int i = 0; i < tlsVersions.length; i++) {
+ result[i] = TlsVersion.forJavaName(tlsVersions[i]);
+ }
+ return Util.immutableList(result);
+ }
+
+ public boolean supportsTlsExtensions() {
+ return supportsTlsExtensions;
+ }
+
+ /** Applies this spec to {@code sslSocket} for {@code route}. */
+ void apply(SSLSocket sslSocket, Route route) {
+ ConnectionSpec specToApply = supportedSpec(sslSocket);
+
+ sslSocket.setEnabledProtocols(specToApply.tlsVersions);
+
+ String[] cipherSuitesToEnable = specToApply.cipherSuites;
+ if (route.shouldSendTlsFallbackIndicator) {
+ // 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 ciphers 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;
+ }
+ }
+ // null means "use default set".
+ if (cipherSuitesToEnable != null) {
+ sslSocket.setEnabledCipherSuites(cipherSuitesToEnable);
+ }
+
+ Platform platform = Platform.get();
+ if (specToApply.supportsTlsExtensions) {
+ platform.configureTlsExtensions(sslSocket, route.address.uriHost, route.address.protocols);
+ }
+ }
+
+ /**
+ * Returns a copy of this that omits cipher suites and TLS versions not
+ * enabled by {@code sslSocket}.
+ */
+ private ConnectionSpec supportedSpec(SSLSocket sslSocket) {
+ String[] cipherSuitesToEnable = null;
+ if (cipherSuites != null) {
+ String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites();
+ cipherSuitesToEnable =
+ Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom);
+ }
+
+ String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols();
+ String[] tlsVersionsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom);
+ return new Builder(this)
+ .cipherSuites(cipherSuitesToEnable)
+ .tlsVersions(tlsVersionsToEnable)
+ .build();
+ }
+
+ @Override public boolean equals(Object other) {
+ if (!(other instanceof ConnectionSpec)) return false;
+
+ ConnectionSpec that = (ConnectionSpec) other;
+ if (this.tls != that.tls) return false;
+
+ if (tls) {
+ if (!Arrays.equals(this.cipherSuites, that.cipherSuites)) return false;
+ if (!Arrays.equals(this.tlsVersions, that.tlsVersions)) return false;
+ if (this.supportsTlsExtensions != that.supportsTlsExtensions) return false;
+ }
+
+ return true;
+ }
+
+ @Override public int hashCode() {
+ int result = 17;
+ if (tls) {
+ result = 31 * result + Arrays.hashCode(cipherSuites);
+ result = 31 * result + Arrays.hashCode(tlsVersions);
+ result = 31 * result + (supportsTlsExtensions ? 0 : 1);
+ }
+ return result;
+ }
+
+ @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 {
+ return "ConnectionSpec()";
+ }
+ }
+
+ public static final class Builder {
+ private boolean tls;
+ private String[] cipherSuites;
+ private String[] tlsVersions;
+ private boolean supportsTlsExtensions;
+
+ Builder(boolean tls) {
+ this.tls = tls;
+ }
+
+ public Builder(ConnectionSpec connectionSpec) {
+ this.tls = connectionSpec.tls;
+ this.cipherSuites = connectionSpec.cipherSuites;
+ this.tlsVersions = connectionSpec.tlsVersions;
+ this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
+ }
+
+ 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;
+ }
+
+ return cipherSuites(strings);
+ }
+
+ Builder cipherSuites(String[] cipherSuites) {
+ this.cipherSuites = cipherSuites; // No defensive copy.
+ return this;
+ }
+
+ public Builder tlsVersions(TlsVersion... tlsVersions) {
+ if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
+
+ // 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;
+ }
+
+ return tlsVersions(strings);
+ }
+
+ Builder tlsVersions(String... tlsVersions) {
+ this.tlsVersions = tlsVersions; // No defensive copy.
+ return this;
+ }
+
+ public Builder supportsTlsExtensions(boolean supportsTlsExtensions) {
+ if (!tls) throw new IllegalStateException("no TLS extensions for cleartext connections");
+ this.supportsTlsExtensions = supportsTlsExtensions;
+ return this;
+ }
+
+ public ConnectionSpec build() {
+ return new ConnectionSpec(this);
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Credentials.java b/okhttp/src/main/java/com/squareup/okhttp/Credentials.java
new file mode 100644
index 0000000..92c128f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Credentials.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 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.io.UnsupportedEncodingException;
+import okio.ByteString;
+
+/** Factory for HTTP authorization credentials. */
+public final class Credentials {
+ private Credentials() {
+ }
+
+ /** Returns an auth credential for the Basic scheme. */
+ public static String basic(String userName, String password) {
+ try {
+ String usernameAndPassword = userName + ":" + password;
+ byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
+ String encoded = ByteString.of(bytes).base64();
+ return "Basic " + encoded;
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError();
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
index 58e06be..95eb7b0 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
@@ -15,7 +15,9 @@
*/
package com.squareup.okhttp;
+import com.squareup.okhttp.Call.AsyncCall;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.http.HttpEngine;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.Iterator;
@@ -27,22 +29,25 @@
/**
* Policy on when async requests are executed.
*
- * <p>Each dispatcher uses an {@link ExecutorService} to run jobs internally. If you
- * supply your own executor, it should be able to run {@link #getMaxRequests the
- * configured maximum} number of jobs concurrently.
+ * <p>Each dispatcher uses an {@link ExecutorService} to run calls internally. If you
+ * supply your own executor, it should be able to run {@linkplain #getMaxRequests the
+ * configured maximum} number of calls concurrently.
*/
public final class Dispatcher {
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
- /** Executes jobs. Created lazily. */
+ /** Executes calls. Created lazily. */
private ExecutorService executorService;
- /** Ready jobs in the order they'll be run. */
- private final Deque<Job> readyJobs = new ArrayDeque<Job>();
+ /** Ready calls in the order they'll be run. */
+ private final Deque<AsyncCall> readyCalls = new ArrayDeque<>();
- /** Running jobs. Includes canceled jobs that haven't finished yet. */
- private final Deque<Job> runningJobs = new ArrayDeque<Job>();
+ /** Running calls. Includes canceled calls that haven't finished yet. */
+ private final Deque<AsyncCall> runningCalls = new ArrayDeque<>();
+
+ /** In-flight synchronous calls. Includes canceled calls that haven't finished yet. */
+ private final Deque<Call> executedCalls = new ArrayDeque<>();
public Dispatcher(ExecutorService executorService) {
this.executorService = executorService;
@@ -61,7 +66,7 @@
/**
* Set the maximum number of requests to execute concurrently. Above this
- * requests queue in memory, waiting for the running jobs to complete.
+ * requests queue in memory, waiting for the running calls to complete.
*
* <p>If more than {@code maxRequests} requests are in flight when this is
* invoked, those requests will remain in flight.
@@ -71,7 +76,7 @@
throw new IllegalArgumentException("max < 1: " + maxRequests);
}
this.maxRequests = maxRequests;
- promoteJobs();
+ promoteCalls();
}
public synchronized int getMaxRequests() {
@@ -92,70 +97,84 @@
throw new IllegalArgumentException("max < 1: " + maxRequestsPerHost);
}
this.maxRequestsPerHost = maxRequestsPerHost;
- promoteJobs();
+ promoteCalls();
}
public synchronized int getMaxRequestsPerHost() {
return maxRequestsPerHost;
}
- synchronized void enqueue(OkHttpClient client, Request request, Response.Receiver receiver) {
- // Copy the client. Otherwise changes (socket factory, redirect policy,
- // etc.) may incorrectly be reflected in the request when it is executed.
- client = client.copyWithDefaults();
- Job job = new Job(this, client, request, receiver);
-
- if (runningJobs.size() < maxRequests && runningJobsForHost(job) < maxRequestsPerHost) {
- runningJobs.add(job);
- getExecutorService().execute(job);
+ synchronized void enqueue(AsyncCall call) {
+ if (runningCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
+ runningCalls.add(call);
+ getExecutorService().execute(call);
} else {
- readyJobs.add(job);
+ readyCalls.add(call);
}
}
- /**
- * Cancel all jobs with the tag {@code tag}. If a canceled job is running it
- * may continue running until it reaches a safe point to finish.
- */
+ /** Cancel all calls with the tag {@code tag}. */
public synchronized void cancel(Object tag) {
- for (Iterator<Job> i = readyJobs.iterator(); i.hasNext(); ) {
- if (Util.equal(tag, i.next().tag())) i.remove();
+ for (AsyncCall call : readyCalls) {
+ if (Util.equal(tag, call.tag())) {
+ call.cancel();
+ }
}
- for (Job job : runningJobs) {
- if (Util.equal(tag, job.tag())) job.canceled = true;
+ for (AsyncCall call : runningCalls) {
+ if (Util.equal(tag, call.tag())) {
+ call.get().canceled = true;
+ HttpEngine engine = call.get().engine;
+ if (engine != null) engine.disconnect();
+ }
+ }
+
+ for (Call call : executedCalls) {
+ if (Util.equal(tag, call.tag())) {
+ call.cancel();
+ }
}
}
- /** Used by {@code Job#run} to signal completion. */
- synchronized void finished(Job job) {
- if (!runningJobs.remove(job)) throw new AssertionError("Job wasn't running!");
- promoteJobs();
+ /** Used by {@code AsyncCall#run} to signal completion. */
+ synchronized void finished(AsyncCall call) {
+ if (!runningCalls.remove(call)) throw new AssertionError("AsyncCall wasn't running!");
+ promoteCalls();
}
- private void promoteJobs() {
- if (runningJobs.size() >= maxRequests) return; // Already running max capacity.
- if (readyJobs.isEmpty()) return; // No ready jobs to promote.
+ private void promoteCalls() {
+ if (runningCalls.size() >= maxRequests) return; // Already running max capacity.
+ if (readyCalls.isEmpty()) return; // No ready calls to promote.
- for (Iterator<Job> i = readyJobs.iterator(); i.hasNext(); ) {
- Job job = i.next();
+ for (Iterator<AsyncCall> i = readyCalls.iterator(); i.hasNext(); ) {
+ AsyncCall call = i.next();
- if (runningJobsForHost(job) < maxRequestsPerHost) {
+ if (runningCallsForHost(call) < maxRequestsPerHost) {
i.remove();
- runningJobs.add(job);
- getExecutorService().execute(job);
+ runningCalls.add(call);
+ getExecutorService().execute(call);
}
- if (runningJobs.size() >= maxRequests) return; // Reached max capacity.
+ if (runningCalls.size() >= maxRequests) return; // Reached max capacity.
}
}
- /** Returns the number of running jobs that share a host with {@code job}. */
- private int runningJobsForHost(Job job) {
+ /** Returns the number of running calls that share a host with {@code call}. */
+ private int runningCallsForHost(AsyncCall call) {
int result = 0;
- for (Job j : runningJobs) {
- if (j.host().equals(job.host())) result++;
+ for (AsyncCall c : runningCalls) {
+ if (c.host().equals(call.host())) result++;
}
return result;
}
+
+ /** Used by {@code Call#execute} to signal it is in-flight. */
+ synchronized void executed(Call call) {
+ executedCalls.add(call);
+ }
+
+ /** Used by {@code Call#execute} to signal completion. */
+ synchronized void finished(Call call) {
+ if (!executedCalls.remove(call)) throw new AssertionError("Call wasn't in-flight!");
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Failure.java b/okhttp/src/main/java/com/squareup/okhttp/Failure.java
deleted file mode 100644
index 51ee2ea..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/Failure.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-/**
- * A failure attempting to retrieve an HTTP response.
- */
-public final class Failure {
- private final Request request;
- private final Throwable exception;
-
- private Failure(Builder builder) {
- this.request = builder.request;
- this.exception = builder.exception;
- }
-
- public Request request() {
- return request;
- }
-
- public Throwable exception() {
- return exception;
- }
-
- public static class Builder {
- private Request request;
- private Throwable exception;
-
- public Builder request(Request request) {
- this.request = request;
- return this;
- }
-
- public Builder exception(Throwable exception) {
- this.exception = exception;
- return this;
- }
-
- public Failure build() {
- return new Failure(this);
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
new file mode 100644
index 0000000..891fbff
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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.Util;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+/**
+ * Fluent API to build <a href="http://www.w3.org/MarkUp/html-spec/html-spec_8.html#SEC8.2.1">HTML
+ * 2.0</a>-compliant form data.
+ */
+public final class FormEncodingBuilder {
+ private static final MediaType CONTENT_TYPE
+ = MediaType.parse("application/x-www-form-urlencoded");
+
+ private final StringBuilder content = new StringBuilder();
+
+ /** Add new key-value pair. */
+ public FormEncodingBuilder add(String name, String value) {
+ if (content.length() > 0) {
+ content.append('&');
+ }
+ try {
+ content.append(URLEncoder.encode(name, "UTF-8"))
+ .append('=')
+ .append(URLEncoder.encode(value, "UTF-8"));
+ } catch (UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ return this;
+ }
+
+ public RequestBody build() {
+ if (content.length() == 0) {
+ throw new IllegalStateException("Form encoded body must have at least one part.");
+ }
+
+ // Convert to bytes so RequestBody.create() doesn't add a charset to the content-type.
+ byte[] contentBytes = content.toString().getBytes(Util.UTF_8);
+ return RequestBody.create(CONTENT_TYPE, contentBytes);
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Headers.java b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
index 1221aa4..2be385c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Headers.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
@@ -17,10 +17,12 @@
package com.squareup.okhttp;
+import com.squareup.okhttp.internal.http.HttpDate;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
+import java.util.Date;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
@@ -49,9 +51,23 @@
this.namesAndValues = builder.namesAndValues.toArray(new String[builder.namesAndValues.size()]);
}
+ private Headers(String[] namesAndValues) {
+ this.namesAndValues = namesAndValues;
+ }
+
/** Returns the last value corresponding to the specified field, or null. */
- public String get(String fieldName) {
- return get(namesAndValues, fieldName);
+ public String get(String name) {
+ return get(namesAndValues, name);
+ }
+
+ /**
+ * Returns the last value corresponding to the specified field parsed as an
+ * HTTP date, or null if either the field is absent or cannot be parsed as a
+ * date.
+ */
+ public Date getDate(String name) {
+ String value = get(name);
+ return value != null ? HttpDate.parse(value) : null;
}
/** Returns the number of field values. */
@@ -61,11 +77,11 @@
/** Returns the field at {@code position} or null if that is out of range. */
public String name(int index) {
- int fieldNameIndex = index * 2;
- if (fieldNameIndex < 0 || fieldNameIndex >= namesAndValues.length) {
+ int nameIndex = index * 2;
+ if (nameIndex < 0 || nameIndex >= namesAndValues.length) {
return null;
}
- return namesAndValues[fieldNameIndex];
+ return namesAndValues[nameIndex];
}
/** Returns the value at {@code index} or null if that is out of range. */
@@ -79,8 +95,8 @@
/** Returns an immutable case-insensitive set of header names. */
public Set<String> names() {
- TreeSet<String> result = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
- for (int i = 0; i < size(); i++) {
+ TreeSet<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ for (int i = 0, size = size(); i < size; i++) {
result.add(name(i));
}
return Collections.unmodifiableSet(result);
@@ -89,9 +105,9 @@
/** Returns an immutable list of the header values for {@code name}. */
public List<String> values(String name) {
List<String> result = null;
- for (int i = 0; i < size(); i++) {
+ for (int i = 0, size = size(); i < size; i++) {
if (name.equalsIgnoreCase(name(i))) {
- if (result == null) result = new ArrayList<String>(2);
+ if (result == null) result = new ArrayList<>(2);
result.add(value(i));
}
}
@@ -100,47 +116,94 @@
: Collections.<String>emptyList();
}
- /** @param fieldNames a case-insensitive set of HTTP header field names. */
- // TODO: it is very weird to request a case-insensitive set as a parameter.
- public Headers getAll(Set<String> fieldNames) {
- Builder result = new Builder();
- for (int i = 0; i < namesAndValues.length; i += 2) {
- String fieldName = namesAndValues[i];
- if (fieldNames.contains(fieldName)) {
- result.add(fieldName, namesAndValues[i + 1]);
- }
- }
- return result.build();
- }
-
public Builder newBuilder() {
Builder result = new Builder();
- result.namesAndValues.addAll(Arrays.asList(namesAndValues));
+ Collections.addAll(result.namesAndValues, namesAndValues);
return result;
}
@Override public String toString() {
StringBuilder result = new StringBuilder();
- for (int i = 0; i < size(); i++) {
+ for (int i = 0, size = size(); i < size; i++) {
result.append(name(i)).append(": ").append(value(i)).append("\n");
}
return result.toString();
}
- private static String get(String[] namesAndValues, String fieldName) {
+ private static String get(String[] namesAndValues, String name) {
for (int i = namesAndValues.length - 2; i >= 0; i -= 2) {
- if (fieldName.equalsIgnoreCase(namesAndValues[i])) {
+ if (name.equalsIgnoreCase(namesAndValues[i])) {
return namesAndValues[i + 1];
}
}
return null;
}
- public static class Builder {
- private final List<String> namesAndValues = new ArrayList<String>(20);
+ /**
+ * Returns headers for the alternating header names and values. There must be
+ * an even number of arguments, and they must alternate between header names
+ * and values.
+ */
+ public static Headers of(String... namesAndValues) {
+ if (namesAndValues == null || namesAndValues.length % 2 != 0) {
+ throw new IllegalArgumentException("Expected alternating header names and values");
+ }
- /** Add an header line containing a field name, a literal colon, and a value. */
- public Builder addLine(String line) {
+ // Make a defensive copy and clean it up.
+ namesAndValues = namesAndValues.clone();
+ for (int i = 0; i < namesAndValues.length; i++) {
+ if (namesAndValues[i] == null) throw new IllegalArgumentException("Headers cannot be null");
+ namesAndValues[i] = namesAndValues[i].trim();
+ }
+
+ // Check for malformed headers.
+ for (int i = 0; i < namesAndValues.length; i += 2) {
+ String name = namesAndValues[i];
+ String value = namesAndValues[i + 1];
+ if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+ throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
+ }
+ }
+
+ return new Headers(namesAndValues);
+ }
+
+ /**
+ * Returns headers for the header names and values in the {@link Map}.
+ */
+ public static Headers of(Map<String, String> headers) {
+ if (headers == null) {
+ throw new IllegalArgumentException("Expected map with header names and values");
+ }
+
+ // Make a defensive copy and clean it up.
+ String[] namesAndValues = new String[headers.size() * 2];
+ int i = 0;
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ if (header.getKey() == null || header.getValue() == null) {
+ throw new IllegalArgumentException("Headers cannot be null");
+ }
+ String name = header.getKey().trim();
+ String value = header.getValue().trim();
+ if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+ throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
+ }
+ namesAndValues[i] = name;
+ namesAndValues[i + 1] = value;
+ i += 2;
+ }
+
+ return new Headers(namesAndValues);
+ }
+
+ public static final class Builder {
+ private final List<String> namesAndValues = new ArrayList<>(20);
+
+ /**
+ * Add a header line without any validation. Only appropriate for headers from the remote peer
+ * or cache.
+ */
+ Builder addLenient(String line) {
int index = line.indexOf(":", 1);
if (index != -1) {
return addLenient(line.substring(0, index), line.substring(index + 1));
@@ -153,31 +216,41 @@
}
}
- /** Add a field with the specified value. */
- public Builder add(String fieldName, String value) {
- if (fieldName == null) throw new IllegalArgumentException("fieldname == null");
- if (value == null) throw new IllegalArgumentException("value == null");
- if (fieldName.length() == 0 || fieldName.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
- throw new IllegalArgumentException("Unexpected header: " + fieldName + ": " + value);
+ /** Add an header line containing a field name, a literal colon, and a value. */
+ public Builder add(String line) {
+ int index = line.indexOf(":");
+ if (index == -1) {
+ throw new IllegalArgumentException("Unexpected header: " + line);
}
- return addLenient(fieldName, value);
+ return add(line.substring(0, index).trim(), line.substring(index + 1));
+ }
+
+ /** Add a field with the specified value. */
+ public Builder add(String name, String value) {
+ if (name == null) throw new IllegalArgumentException("name == null");
+ if (value == null) throw new IllegalArgumentException("value == null");
+ if (name.length() == 0 || name.indexOf('\0') != -1 || value.indexOf('\0') != -1) {
+ throw new IllegalArgumentException("Unexpected header: " + name + ": " + value);
+ }
+ return addLenient(name, value);
}
/**
* Add a field with the specified value without any validation. Only
* appropriate for headers from the remote peer.
*/
- private Builder addLenient(String fieldName, String value) {
- namesAndValues.add(fieldName);
+ private Builder addLenient(String name, String value) {
+ namesAndValues.add(name);
namesAndValues.add(value.trim());
return this;
}
- public Builder removeAll(String fieldName) {
+ public Builder removeAll(String name) {
for (int i = 0; i < namesAndValues.size(); i += 2) {
- if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
- namesAndValues.remove(i); // field name
+ if (name.equalsIgnoreCase(namesAndValues.get(i))) {
+ namesAndValues.remove(i); // name
namesAndValues.remove(i); // value
+ i -= 2;
}
}
return this;
@@ -187,16 +260,16 @@
* Set a field with the specified value. If the field is not found, it is
* added. If the field is found, the existing values are replaced.
*/
- public Builder set(String fieldName, String value) {
- removeAll(fieldName);
- add(fieldName, value);
+ public Builder set(String name, String value) {
+ removeAll(name);
+ add(name, value);
return this;
}
- /** Equivalent to {@code build().get(fieldName)}, but potentially faster. */
- public String get(String fieldName) {
+ /** Equivalent to {@code build().get(name)}, but potentially faster. */
+ public String get(String name) {
for (int i = namesAndValues.size() - 2; i >= 0; i -= 2) {
- if (fieldName.equalsIgnoreCase(namesAndValues.get(i))) {
+ if (name.equalsIgnoreCase(namesAndValues.get(i))) {
return namesAndValues.get(i + 1);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Interceptor.java b/okhttp/src/main/java/com/squareup/okhttp/Interceptor.java
new file mode 100644
index 0000000..03325be
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Interceptor.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2014 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.io.IOException;
+
+/**
+ * Observes, modifies, and potentially short-circuits requests going out and the corresponding
+ * requests coming back in. Typically interceptors will be used to add, remove, or transform headers
+ * on the request or response.
+ */
+public interface Interceptor {
+ Response intercept(Chain chain) throws IOException;
+
+ interface Chain {
+ Request request();
+ Response proceed(Request request) throws IOException;
+ Connection connection();
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Job.java b/okhttp/src/main/java/com/squareup/okhttp/Job.java
deleted file mode 100644
index 721acc8..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/Job.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-import com.squareup.okhttp.internal.NamedRunnable;
-import com.squareup.okhttp.internal.http.HttpAuthenticator;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
-import com.squareup.okhttp.internal.http.OkHeaders;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.ProtocolException;
-import java.net.Proxy;
-import java.net.URL;
-import okio.BufferedSink;
-import okio.Okio;
-import okio.Source;
-
-import static com.squareup.okhttp.internal.Util.getEffectivePort;
-import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_PERM;
-import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_TEMP;
-import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MULT_CHOICE;
-import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_PROXY_AUTH;
-import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_SEE_OTHER;
-import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_UNAUTHORIZED;
-import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
-
-final class Job extends NamedRunnable {
- private final Dispatcher dispatcher;
- private final OkHttpClient client;
- private final Response.Receiver responseReceiver;
- private int redirectionCount;
-
- volatile boolean canceled;
-
- /** The request; possibly a consequence of redirects or auth headers. */
- private Request request;
- HttpEngine engine;
-
- public Job(Dispatcher dispatcher, OkHttpClient client, Request request,
- Response.Receiver responseReceiver) {
- super("OkHttp %s", request.urlString());
- this.dispatcher = dispatcher;
- this.client = client;
- this.request = request;
- this.responseReceiver = responseReceiver;
- }
-
- String host() {
- return request.url().getHost();
- }
-
- Request request() {
- return request;
- }
-
- Object tag() {
- return request.tag();
- }
-
- @Override protected void execute() {
- try {
- Response response = getResponse();
- if (response != null && !canceled) {
- responseReceiver.onResponse(response);
- }
- } catch (IOException e) {
- responseReceiver.onFailure(new Failure.Builder()
- .request(request)
- .exception(e)
- .build());
- } finally {
- engine.close(); // Close the connection if it isn't already.
- dispatcher.finished(this);
- }
- }
-
- /**
- * Performs the request and returns the response. May return null if this job
- * was canceled.
- */
- Response getResponse() throws IOException {
- Response redirectedBy = null;
-
- // Copy body metadata to the appropriate request headers.
- Request.Body body = request.body();
- if (body != null) {
- MediaType contentType = body.contentType();
- if (contentType == null) throw new IllegalStateException("contentType == null");
-
- Request.Builder requestBuilder = request.newBuilder();
- requestBuilder.header("Content-Type", contentType.toString());
-
- long contentLength = body.contentLength();
- if (contentLength != -1) {
- requestBuilder.header("Content-Length", Long.toString(contentLength));
- requestBuilder.removeHeader("Transfer-Encoding");
- } else {
- requestBuilder.header("Transfer-Encoding", "chunked");
- requestBuilder.removeHeader("Content-Length");
- }
-
- request = requestBuilder.build();
- }
-
- // Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
- engine = new HttpEngine(client, request, false, null, null, null, null);
-
- while (true) {
- if (canceled) return null;
-
- try {
- engine.sendRequest();
-
- if (body != null) {
- BufferedSink sink = Okio.buffer(engine.getRequestBody());
- body.writeTo(sink);
- sink.flush();
- }
-
- engine.readResponse();
- } catch (IOException e) {
- HttpEngine retryEngine = engine.recover(e);
- if (retryEngine != null) {
- engine = retryEngine;
- continue;
- }
-
- // Give up; recovery is not possible.
- throw e;
- }
-
- Response response = engine.getResponse();
- Request redirect = processResponse(engine, response);
-
- if (redirect == null) {
- engine.releaseConnection();
- return response.newBuilder()
- .body(new RealResponseBody(response, engine.getResponseBody()))
- .priorResponse(redirectedBy)
- .build();
- }
-
- if (!sameConnection(request, redirect)) {
- engine.releaseConnection();
- }
-
- Connection connection = engine.close();
- redirectedBy = response.newBuilder().priorResponse(redirectedBy).build(); // Chained.
- request = redirect;
- engine = new HttpEngine(client, request, false, connection, null, null, null);
- }
- }
-
- /**
- * Figures out the HTTP request to make in response to receiving {@code
- * response}. This will either add authentication headers or follow
- * redirects. If a follow-up is either unnecessary or not applicable, this
- * returns null.
- */
- private Request processResponse(HttpEngine engine, Response response) throws IOException {
- Request request = response.request();
- Proxy selectedProxy = engine.getRoute() != null
- ? engine.getRoute().getProxy()
- : client.getProxy();
- int responseCode = response.code();
-
- switch (responseCode) {
- case HTTP_PROXY_AUTH:
- if (selectedProxy.type() != Proxy.Type.HTTP) {
- throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
- }
- // fall-through
- case HTTP_UNAUTHORIZED:
- return HttpAuthenticator.processAuthHeader(
- client.getAuthenticator(), response, selectedProxy);
-
- case HTTP_MULT_CHOICE:
- case HTTP_MOVED_PERM:
- case HTTP_MOVED_TEMP:
- case HTTP_SEE_OTHER:
- case HTTP_TEMP_REDIRECT:
- if (!client.getFollowProtocolRedirects()) {
- return null; // This client has is configured to not follow redirects.
- }
-
- if (++redirectionCount > HttpURLConnectionImpl.MAX_REDIRECTS) {
- throw new ProtocolException("Too many redirects: " + redirectionCount);
- }
-
- String method = request.method();
- if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) {
- // "If the 307 status code is received in response to a request other than GET or HEAD,
- // the user agent MUST NOT automatically redirect the request"
- return null;
- }
-
- String location = response.header("Location");
- if (location == null) {
- return null;
- }
-
- URL url = new URL(request.url(), location);
- if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) {
- return null; // Don't follow redirects to unsupported protocols.
- }
-
- return this.request.newBuilder().url(url).build();
-
- default:
- return null;
- }
- }
-
- static boolean sameConnection(Request a, Request b) {
- return a.url().getHost().equals(b.url().getHost())
- && getEffectivePort(a.url()) == getEffectivePort(b.url())
- && a.url().getProtocol().equals(b.url().getProtocol());
- }
-
- static class RealResponseBody extends Response.Body {
- private final Response response;
- private final Source source;
-
- /** Multiple calls to {@link #byteStream} must return the same instance. */
- private InputStream in;
-
- RealResponseBody(Response response, Source source) {
- this.response = response;
- this.source = source;
- }
-
- @Override public boolean ready() throws IOException {
- return true;
- }
-
- @Override public MediaType contentType() {
- String contentType = response.header("Content-Type");
- return contentType != null ? MediaType.parse(contentType) : null;
- }
-
- @Override public long contentLength() {
- return OkHeaders.contentLength(response);
- }
-
- @Override public Source source() {
- return source;
- }
-
- @Override public InputStream byteStream() {
- InputStream result = in;
- return result != null
- ? result
- : (in = Okio.buffer(source).inputStream());
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/MediaType.java b/okhttp/src/main/java/com/squareup/okhttp/MediaType.java
index 2c09596..4d2f1fc 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/MediaType.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/MediaType.java
@@ -29,7 +29,7 @@
private static final String QUOTED = "\"([^\"]*)\"";
private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN);
private static final Pattern PARAMETER = Pattern.compile(
- ";\\s*" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + ")");
+ ";\\s*(?:" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + "))?");
private final String mediaType;
private final String type;
@@ -61,10 +61,13 @@
String name = parameter.group(1);
if (name == null || !name.equalsIgnoreCase("charset")) continue;
- if (charset != null) throw new IllegalArgumentException("Multiple charsets: " + string);
- charset = parameter.group(2) != null
+ String charsetParameter = parameter.group(2) != null
? parameter.group(2) // Value is a token.
: parameter.group(3); // Value is a quoted string.
+ if (charset != null && !charsetParameter.equalsIgnoreCase(charset)) {
+ throw new IllegalArgumentException("Multiple different charsets: " + string);
+ }
+ charset = charsetParameter;
}
return new MediaType(string, type, subtype, charset);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/MultipartBuilder.java b/okhttp/src/main/java/com/squareup/okhttp/MultipartBuilder.java
new file mode 100644
index 0000000..be24c7b
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/MultipartBuilder.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2014 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.Util;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import okio.BufferedSink;
+import okio.ByteString;
+
+/**
+ * Fluent API to build <a href="http://www.ietf.org/rfc/rfc2387.txt">RFC
+ * 2387</a>-compliant request bodies.
+ */
+public final class MultipartBuilder {
+ /**
+ * The "mixed" subtype of "multipart" is intended for use when the body
+ * parts are independent and need to be bundled in a particular order. Any
+ * "multipart" subtypes that an implementation does not recognize must be
+ * treated as being of subtype "mixed".
+ */
+ public static final MediaType MIXED = MediaType.parse("multipart/mixed");
+
+ /**
+ * The "multipart/alternative" type is syntactically identical to
+ * "multipart/mixed", but the semantics are different. In particular, each
+ * of the body parts is an "alternative" version of the same information.
+ */
+ public static final MediaType ALTERNATIVE = MediaType.parse("multipart/alternative");
+
+ /**
+ * This type is syntactically identical to "multipart/mixed", but the
+ * semantics are different. In particular, in a digest, the default {@code
+ * Content-Type} value for a body part is changed from "text/plain" to
+ * "message/rfc822".
+ */
+ public static final MediaType DIGEST = MediaType.parse("multipart/digest");
+
+ /**
+ * This type is syntactically identical to "multipart/mixed", but the
+ * semantics are different. In particular, in a parallel entity, the order
+ * of body parts is not significant.
+ */
+ public static final MediaType PARALLEL = MediaType.parse("multipart/parallel");
+
+ /**
+ * The media-type multipart/form-data follows the rules of all multipart
+ * MIME data streams as outlined in RFC 2046. In forms, there are a series
+ * of fields to be supplied by the user who fills out the form. Each field
+ * has a name. Within a given form, the names are unique.
+ */
+ public static final MediaType FORM = MediaType.parse("multipart/form-data");
+
+ private static final byte[] COLONSPACE = { ':', ' ' };
+ private static final byte[] CRLF = { '\r', '\n' };
+ private static final byte[] DASHDASH = { '-', '-' };
+
+ private final ByteString boundary;
+ private MediaType type = MIXED;
+
+ // Parallel lists of nullable headers and non-null bodies.
+ private final List<Headers> partHeaders = new ArrayList<>();
+ private final List<RequestBody> partBodies = new ArrayList<>();
+
+ /** Creates a new multipart builder that uses a random boundary token. */
+ public MultipartBuilder() {
+ this(UUID.randomUUID().toString());
+ }
+
+ /**
+ * Creates a new multipart builder that uses {@code boundary} to separate
+ * parts. Prefer the no-argument constructor to defend against injection
+ * attacks.
+ */
+ public MultipartBuilder(String boundary) {
+ this.boundary = ByteString.encodeUtf8(boundary);
+ }
+
+ /**
+ * Set the MIME type. Expected values for {@code type} are {@link #MIXED} (the
+ * default), {@link #ALTERNATIVE}, {@link #DIGEST}, {@link #PARALLEL} and
+ * {@link #FORM}.
+ */
+ public MultipartBuilder type(MediaType type) {
+ if (type == null) {
+ throw new NullPointerException("type == null");
+ }
+ if (!type.type().equals("multipart")) {
+ throw new IllegalArgumentException("multipart != " + type);
+ }
+ this.type = type;
+ return this;
+ }
+
+ /** Add a part to the body. */
+ public MultipartBuilder addPart(RequestBody body) {
+ return addPart(null, body);
+ }
+
+ /** Add a part to the body. */
+ public MultipartBuilder addPart(Headers headers, RequestBody body) {
+ if (body == null) {
+ throw new NullPointerException("body == null");
+ }
+ if (headers != null && headers.get("Content-Type") != null) {
+ throw new IllegalArgumentException("Unexpected header: Content-Type");
+ }
+ if (headers != null && headers.get("Content-Length") != null) {
+ throw new IllegalArgumentException("Unexpected header: Content-Length");
+ }
+
+ partHeaders.add(headers);
+ partBodies.add(body);
+ return this;
+ }
+
+ /**
+ * Appends a quoted-string to a StringBuilder.
+ *
+ * <p>RFC 2388 is rather vague about how one should escape special characters
+ * in form-data parameters, and as it turns out Firefox and Chrome actually
+ * do rather different things, and both say in their comments that they're
+ * not really sure what the right approach is. We go with Chrome's behavior
+ * (which also experimentally seems to match what IE does), but if you
+ * actually want to have a good chance of things working, please avoid
+ * double-quotes, newlines, percent signs, and the like in your field names.
+ */
+ private static StringBuilder appendQuotedString(StringBuilder target, String key) {
+ target.append('"');
+ for (int i = 0, len = key.length(); i < len; i++) {
+ char ch = key.charAt(i);
+ switch (ch) {
+ case '\n':
+ target.append("%0A");
+ break;
+ case '\r':
+ target.append("%0D");
+ break;
+ case '"':
+ target.append("%22");
+ break;
+ default:
+ target.append(ch);
+ break;
+ }
+ }
+ target.append('"');
+ return target;
+ }
+
+ /** Add a form data part to the body. */
+ public MultipartBuilder addFormDataPart(String name, String value) {
+ return addFormDataPart(name, null, RequestBody.create(null, value));
+ }
+
+ /** Add a form data part to the body. */
+ public MultipartBuilder addFormDataPart(String name, String filename, RequestBody value) {
+ if (name == null) {
+ throw new NullPointerException("name == null");
+ }
+ StringBuilder disposition = new StringBuilder("form-data; name=");
+ appendQuotedString(disposition, name);
+
+ if (filename != null) {
+ disposition.append("; filename=");
+ appendQuotedString(disposition, filename);
+ }
+
+ return addPart(Headers.of("Content-Disposition", disposition.toString()), value);
+ }
+
+ /** Assemble the specified parts into a request body. */
+ public RequestBody build() {
+ if (partHeaders.isEmpty()) {
+ throw new IllegalStateException("Multipart body must have at least one part.");
+ }
+ return new MultipartRequestBody(type, boundary, partHeaders, partBodies);
+ }
+
+ private static final class MultipartRequestBody extends RequestBody {
+ private final ByteString boundary;
+ private final MediaType contentType;
+ private final List<Headers> partHeaders;
+ private final List<RequestBody> partBodies;
+
+ public MultipartRequestBody(MediaType type, ByteString boundary, List<Headers> partHeaders,
+ List<RequestBody> partBodies) {
+ if (type == null) throw new NullPointerException("type == null");
+
+ this.boundary = boundary;
+ this.contentType = MediaType.parse(type + "; boundary=" + boundary.utf8());
+ this.partHeaders = Util.immutableList(partHeaders);
+ this.partBodies = Util.immutableList(partBodies);
+ }
+
+ @Override public MediaType contentType() {
+ return contentType;
+ }
+
+ @Override public long contentLength() throws IOException {
+ return -1L;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ for (int p = 0, partCount = partHeaders.size(); p < partCount; p++) {
+ Headers headers = partHeaders.get(p);
+ RequestBody body = partBodies.get(p);
+
+ sink.write(DASHDASH);
+ sink.write(boundary);
+ sink.write(CRLF);
+
+ if (headers != null) {
+ for (int h = 0, headerCount = headers.size(); h < headerCount; h++) {
+ sink.writeUtf8(headers.name(h))
+ .write(COLONSPACE)
+ .writeUtf8(headers.value(h))
+ .write(CRLF);
+ }
+ }
+
+ MediaType contentType = body.contentType();
+ if (contentType != null) {
+ sink.writeUtf8("Content-Type: ")
+ .writeUtf8(contentType.toString())
+ .write(CRLF);
+ }
+
+ long contentLength = body.contentLength();
+ if (contentLength != -1) {
+ sink.writeUtf8("Content-Length: ")
+ .writeUtf8(Long.toString(contentLength))
+ .write(CRLF);
+ }
+
+ sink.write(CRLF);
+ partBodies.get(p).writeTo(sink);
+ sink.write(CRLF);
+ }
+
+ sink.write(DASHDASH);
+ sink.write(boundary);
+ sink.write(DASHDASH);
+ sink.write(CRLF);
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java b/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java
deleted file mode 100644
index e8ca5ea..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/OkAuthenticator.java
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp;
-
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.net.Proxy;
-import java.net.URL;
-import java.util.List;
-import okio.ByteString;
-
-/**
- * Responds to authentication challenges from the remote web or proxy server by
- * returning credentials.
- */
-public interface OkAuthenticator {
- /**
- * Returns a credential that satisfies the authentication challenge made by
- * {@code url}. Returns null if the challenge cannot be satisfied. This method
- * is called in response to an HTTP 401 unauthorized status code sent by the
- * origin server.
- *
- * @param challenges parsed "WWW-Authenticate" challenge headers from the HTTP
- * response.
- */
- Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
-
- /**
- * Returns a credential that satisfies the authentication challenge made by
- * {@code proxy}. Returns null if the challenge cannot be satisfied. This
- * method is called in response to an HTTP 401 unauthorized status code sent
- * by the proxy server.
- *
- * @param challenges parsed "Proxy-Authenticate" challenge headers from the
- * HTTP response.
- */
- Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException;
-
- /** An RFC 2617 challenge. */
- public final class Challenge {
- private final String scheme;
- private final String realm;
-
- public Challenge(String scheme, String realm) {
- this.scheme = scheme;
- this.realm = realm;
- }
-
- /** Returns the authentication scheme, like {@code Basic}. */
- public String getScheme() {
- return scheme;
- }
-
- /** Returns the protection space. */
- public String getRealm() {
- return realm;
- }
-
- @Override public boolean equals(Object o) {
- return o instanceof Challenge
- && ((Challenge) o).scheme.equals(scheme)
- && ((Challenge) o).realm.equals(realm);
- }
-
- @Override public int hashCode() {
- return scheme.hashCode() + 31 * realm.hashCode();
- }
-
- @Override public String toString() {
- return scheme + " realm=\"" + realm + "\"";
- }
- }
-
- /** An RFC 2617 credential. */
- public final class Credential {
- private final String headerValue;
-
- private Credential(String headerValue) {
- this.headerValue = headerValue;
- }
-
- /** Returns an auth credential for the Basic scheme. */
- public static Credential basic(String userName, String password) {
- try {
- String usernameAndPassword = userName + ":" + password;
- byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1");
- String encoded = ByteString.of(bytes).base64();
- return new Credential("Basic " + encoded);
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError();
- }
- }
-
- public String getHeaderValue() {
- return headerValue;
- }
-
- @Override public boolean equals(Object o) {
- return o instanceof Credential && ((Credential) o).headerValue.equals(headerValue);
- }
-
- @Override public int hashCode() {
- return headerValue.hashCode();
- }
-
- @Override public String toString() {
- return headerValue;
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 0bb98c5..56b55f9 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -15,22 +15,20 @@
*/
package com.squareup.okhttp;
+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.HttpAuthenticator;
-import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;
-import com.squareup.okhttp.internal.http.HttpsURLConnectionImpl;
-import com.squareup.okhttp.internal.http.ResponseCacheAdapter;
+import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import com.squareup.okhttp.internal.http.Transport;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
import java.io.IOException;
import java.net.CookieHandler;
-import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.ProxySelector;
-import java.net.ResponseCache;
-import java.net.URL;
import java.net.URLConnection;
-import java.net.URLStreamHandler;
-import java.net.URLStreamHandlerFactory;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.List;
@@ -39,64 +37,186 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
-import okio.ByteString;
/**
* Configures and creates HTTP connections. Most applications can use a single
* OkHttpClient for all of their HTTP requests - benefiting from a shared
* response cache, thread pool, connection re-use, etc.
*
- * Instances of OkHttpClient are intended to be fully configured before they're
+ * <p>Instances of OkHttpClient are intended to be fully configured before they're
* shared - once shared they should be treated as immutable and can safely be used
* to concurrently open new connections. If required, threads can call
* {@link #clone()} to make a shallow copy of the OkHttpClient that can be
* safely modified with further configuration changes.
*/
-public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable {
+public class OkHttpClient implements Cloneable {
+ private static final List<Protocol> DEFAULT_PROTOCOLS = Util.immutableList(
+ Protocol.HTTP_2, Protocol.SPDY_3, Protocol.HTTP_1_1);
+
+ private static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
+ ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS, ConnectionSpec.CLEARTEXT);
+
+ 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);
+ }
+
+ @Override public void setCache(OkHttpClient client, InternalCache internalCache) {
+ client.setInternalCache(internalCache);
+ }
+
+ @Override public InternalCache internalCache(OkHttpClient client) {
+ return client.internalCache();
+ }
+
+ @Override public void recycle(ConnectionPool pool, Connection connection) {
+ pool.recycle(connection);
+ }
+
+ @Override public RouteDatabase routeDatabase(OkHttpClient client) {
+ return client.routeDatabase();
+ }
+
+ @Override public Network network(OkHttpClient client) {
+ return client.network;
+ }
+
+ @Override public void setNetwork(OkHttpClient client, Network network) {
+ client.network = network;
+ }
+
+ @Override public void connectAndSetOwner(OkHttpClient client, Connection connection,
+ HttpEngine owner, Request request) throws IOException {
+ connection.connectAndSetOwner(client, owner, request);
+ }
+
+ @Override
+ public void callEnqueue(Call call, Callback responseCallback, boolean forWebSocket) {
+ 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 void connectionSetOwner(Connection connection, Object owner) {
+ connection.setOwner(owner);
+ }
+ };
+ }
+
+ /** Lazily-initialized. */
+ private static SSLSocketFactory defaultSslSocketFactory;
private final RouteDatabase routeDatabase;
private Dispatcher dispatcher;
private Proxy proxy;
private List<Protocol> protocols;
+ private List<ConnectionSpec> connectionSpecs;
+ private final List<Interceptor> interceptors = new ArrayList<>();
+ private final List<Interceptor> networkInterceptors = new ArrayList<>();
private ProxySelector proxySelector;
private CookieHandler cookieHandler;
- private OkResponseCache responseCache;
+
+ /** Non-null if this client is caching; possibly by {@code cache}. */
+ private InternalCache internalCache;
+ private Cache cache;
+
private SocketFactory socketFactory;
private SSLSocketFactory sslSocketFactory;
private HostnameVerifier hostnameVerifier;
- private OkAuthenticator authenticator;
+ private CertificatePinner certificatePinner;
+ private Authenticator authenticator;
private ConnectionPool connectionPool;
- private HostResolver hostResolver;
- private boolean followProtocolRedirects = true;
+ private Network network;
+ private boolean followSslRedirects = true;
+ private boolean followRedirects = true;
+ private boolean retryOnConnectionFailure = true;
private int connectTimeout;
private int readTimeout;
+ private int writeTimeout;
public OkHttpClient() {
routeDatabase = new RouteDatabase();
dispatcher = new Dispatcher();
}
+ private OkHttpClient(OkHttpClient okHttpClient) {
+ this.routeDatabase = okHttpClient.routeDatabase;
+ this.dispatcher = okHttpClient.dispatcher;
+ this.proxy = okHttpClient.proxy;
+ this.protocols = okHttpClient.protocols;
+ this.connectionSpecs = okHttpClient.connectionSpecs;
+ this.interceptors.addAll(okHttpClient.interceptors);
+ this.networkInterceptors.addAll(okHttpClient.networkInterceptors);
+ this.proxySelector = okHttpClient.proxySelector;
+ this.cookieHandler = okHttpClient.cookieHandler;
+ this.cache = okHttpClient.cache;
+ this.internalCache = cache != null ? cache.internalCache : okHttpClient.internalCache;
+ this.socketFactory = okHttpClient.socketFactory;
+ this.sslSocketFactory = okHttpClient.sslSocketFactory;
+ this.hostnameVerifier = okHttpClient.hostnameVerifier;
+ this.certificatePinner = okHttpClient.certificatePinner;
+ this.authenticator = okHttpClient.authenticator;
+ this.connectionPool = okHttpClient.connectionPool;
+ this.network = okHttpClient.network;
+ this.followSslRedirects = okHttpClient.followSslRedirects;
+ this.followRedirects = okHttpClient.followRedirects;
+ this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
+ this.connectTimeout = okHttpClient.connectTimeout;
+ this.readTimeout = okHttpClient.readTimeout;
+ this.writeTimeout = okHttpClient.writeTimeout;
+ }
+
/**
* Sets the default connect timeout for new connections. A value of 0 means no timeout.
*
* @see URLConnection#setConnectTimeout(int)
*/
- public void setConnectTimeout(long timeout, TimeUnit unit) {
- if (timeout < 0) {
- throw new IllegalArgumentException("timeout < 0");
- }
- if (unit == null) {
- throw new IllegalArgumentException("unit == null");
- }
+ public final void setConnectTimeout(long timeout, TimeUnit unit) {
+ if (timeout < 0) throw new IllegalArgumentException("timeout < 0");
+ if (unit == null) throw new IllegalArgumentException("unit == null");
long millis = unit.toMillis(timeout);
- if (millis > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("Timeout too large.");
- }
+ if (millis > Integer.MAX_VALUE) throw new IllegalArgumentException("Timeout too large.");
connectTimeout = (int) millis;
}
/** Default connect timeout (in milliseconds). */
- public int getConnectTimeout() {
+ public final int getConnectTimeout() {
return connectTimeout;
}
@@ -105,37 +225,47 @@
*
* @see URLConnection#setReadTimeout(int)
*/
- public void setReadTimeout(long timeout, TimeUnit unit) {
- if (timeout < 0) {
- throw new IllegalArgumentException("timeout < 0");
- }
- if (unit == null) {
- throw new IllegalArgumentException("unit == null");
- }
+ public final void setReadTimeout(long timeout, TimeUnit unit) {
+ if (timeout < 0) throw new IllegalArgumentException("timeout < 0");
+ if (unit == null) throw new IllegalArgumentException("unit == null");
long millis = unit.toMillis(timeout);
- if (millis > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("Timeout too large.");
- }
+ if (millis > Integer.MAX_VALUE) throw new IllegalArgumentException("Timeout too large.");
readTimeout = (int) millis;
}
/** Default read timeout (in milliseconds). */
- public int getReadTimeout() {
+ public final int getReadTimeout() {
return readTimeout;
}
/**
+ * Sets the default write timeout for new connections. A value of 0 means no timeout.
+ */
+ public final void setWriteTimeout(long timeout, TimeUnit unit) {
+ if (timeout < 0) throw new IllegalArgumentException("timeout < 0");
+ if (unit == null) throw new IllegalArgumentException("unit == null");
+ long millis = unit.toMillis(timeout);
+ if (millis > Integer.MAX_VALUE) throw new IllegalArgumentException("Timeout too large.");
+ writeTimeout = (int) millis;
+ }
+
+ /** Default write timeout (in milliseconds). */
+ public final int getWriteTimeout() {
+ return writeTimeout;
+ }
+
+ /**
* Sets the HTTP proxy that will be used by connections created by this
* client. This takes precedence over {@link #setProxySelector}, which is
* only honored when this proxy is null (which it is by default). To disable
* proxy use completely, call {@code setProxy(Proxy.NO_PROXY)}.
*/
- public OkHttpClient setProxy(Proxy proxy) {
+ public final OkHttpClient setProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
- public Proxy getProxy() {
+ public final Proxy getProxy() {
return proxy;
}
@@ -148,12 +278,12 @@
* <p>If unset, the {@link ProxySelector#getDefault() system-wide default}
* proxy selector will be used.
*/
- public OkHttpClient setProxySelector(ProxySelector proxySelector) {
+ public final OkHttpClient setProxySelector(ProxySelector proxySelector) {
this.proxySelector = proxySelector;
return this;
}
- public ProxySelector getProxySelector() {
+ public final ProxySelector getProxySelector() {
return proxySelector;
}
@@ -164,35 +294,33 @@
* <p>If unset, the {@link CookieHandler#getDefault() system-wide default}
* cookie handler will be used.
*/
- public OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
+ public final OkHttpClient setCookieHandler(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
return this;
}
- public CookieHandler getCookieHandler() {
+ public final CookieHandler getCookieHandler() {
return cookieHandler;
}
- /**
- * Sets the response cache to be used to read and write cached responses.
- */
- public OkHttpClient setResponseCache(ResponseCache responseCache) {
- return setOkResponseCache(toOkResponseCache(responseCache));
+ /** Sets the response cache to be used to read and write cached responses. */
+ final void setInternalCache(InternalCache internalCache) {
+ this.internalCache = internalCache;
+ this.cache = null;
}
- public ResponseCache getResponseCache() {
- return responseCache instanceof ResponseCacheAdapter
- ? ((ResponseCacheAdapter) responseCache).getDelegate()
- : null;
+ final InternalCache internalCache() {
+ return internalCache;
}
- public OkHttpClient setOkResponseCache(OkResponseCache responseCache) {
- this.responseCache = responseCache;
+ public final OkHttpClient setCache(Cache cache) {
+ this.cache = cache;
+ this.internalCache = null;
return this;
}
- public OkResponseCache getOkResponseCache() {
- return responseCache;
+ public final Cache getCache() {
+ return cache;
}
/**
@@ -201,12 +329,12 @@
* <p>If unset, the {@link SocketFactory#getDefault() system-wide default}
* socket factory will be used.
*/
- public OkHttpClient setSocketFactory(SocketFactory socketFactory) {
+ public final OkHttpClient setSocketFactory(SocketFactory socketFactory) {
this.socketFactory = socketFactory;
return this;
}
- public SocketFactory getSocketFactory() {
+ public final SocketFactory getSocketFactory() {
return socketFactory;
}
@@ -215,12 +343,12 @@
*
* <p>If unset, a lazily created SSL socket factory will be used.
*/
- public OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
+ public final OkHttpClient setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
return this;
}
- public SSLSocketFactory getSslSocketFactory() {
+ public final SSLSocketFactory getSslSocketFactory() {
return sslSocketFactory;
}
@@ -228,32 +356,45 @@
* Sets the verifier used to confirm that response certificates apply to
* requested hostnames for HTTPS connections.
*
- * <p>If unset, the
- * {@link javax.net.ssl.HttpsURLConnection#getDefaultHostnameVerifier()
- * system-wide default} hostname verifier will be used.
+ * <p>If unset, a default hostname verifier will be used.
*/
- public OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+ public final OkHttpClient setHostnameVerifier(HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
return this;
}
- public HostnameVerifier getHostnameVerifier() {
+ public final HostnameVerifier getHostnameVerifier() {
return hostnameVerifier;
}
/**
+ * Sets the certificate pinner that constrains which certificates are trusted.
+ * By default HTTPS connections rely on only the {@link #setSslSocketFactory
+ * SSL socket factory} to establish trust. Pinning certificates avoids the
+ * need to trust certificate authorities.
+ */
+ public final OkHttpClient setCertificatePinner(CertificatePinner certificatePinner) {
+ this.certificatePinner = certificatePinner;
+ return this;
+ }
+
+ public final CertificatePinner getCertificatePinner() {
+ return certificatePinner;
+ }
+
+ /**
* Sets the authenticator used to respond to challenges from the remote web
* server or proxy server.
*
* <p>If unset, the {@link java.net.Authenticator#setDefault system-wide default}
* authenticator will be used.
*/
- public OkHttpClient setAuthenticator(OkAuthenticator authenticator) {
+ public final OkHttpClient setAuthenticator(Authenticator authenticator) {
this.authenticator = authenticator;
return this;
}
- public OkAuthenticator getAuthenticator() {
+ public final Authenticator getAuthenticator() {
return authenticator;
}
@@ -263,12 +404,12 @@
* <p>If unset, the {@link ConnectionPool#getDefault() system-wide
* default} connection pool will be used.
*/
- public OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
+ public final OkHttpClient setConnectionPool(ConnectionPool connectionPool) {
this.connectionPool = connectionPool;
return this;
}
- public ConnectionPool getConnectionPool() {
+ public final ConnectionPool getConnectionPool() {
return connectionPool;
}
@@ -279,16 +420,51 @@
* <p>If unset, protocol redirects will be followed. This is different than
* the built-in {@code HttpURLConnection}'s default.
*/
- public OkHttpClient setFollowProtocolRedirects(boolean followProtocolRedirects) {
- this.followProtocolRedirects = followProtocolRedirects;
+ public final OkHttpClient setFollowSslRedirects(boolean followProtocolRedirects) {
+ this.followSslRedirects = followProtocolRedirects;
return this;
}
- public boolean getFollowProtocolRedirects() {
- return followProtocolRedirects;
+ public final boolean getFollowSslRedirects() {
+ return followSslRedirects;
}
- public RouteDatabase getRoutesDatabase() {
+ /** Configure this client to follow redirects. If unset, redirects be followed. */
+ public final void setFollowRedirects(boolean followRedirects) {
+ this.followRedirects = followRedirects;
+ }
+
+ public final boolean getFollowRedirects() {
+ return followRedirects;
+ }
+
+ /**
+ * Configure this client to retry or not when a connectivity problem is encountered. By default,
+ * this client silently recovers from the following problems:
+ *
+ * <ul>
+ * <li><strong>Unreachable IP addresses.</strong> If the URL's host has multiple IP addresses,
+ * failure to reach any individual IP address doesn't fail the overall request. This can
+ * increase availability of multi-homed services.
+ * <li><strong>Stale pooled connections.</strong> The {@link ConnectionPool} reuses sockets
+ * to decrease request latency, but these connections will occasionally time out.
+ * <li><strong>Unreachable proxy servers.</strong> A {@link ProxySelector} can be used to
+ * attempt multiple proxy servers in sequence, eventually falling back to a direct
+ * connection.
+ * </ul>
+ *
+ * Set this to false to avoid retrying requests when doing so is destructive. In this case the
+ * calling application should do its own recovery of connectivity failures.
+ */
+ public final void setRetryOnConnectionFailure(boolean retryOnConnectionFailure) {
+ this.retryOnConnectionFailure = retryOnConnectionFailure;
+ }
+
+ public final boolean getRetryOnConnectionFailure() {
+ return retryOnConnectionFailure;
+ }
+
+ final RouteDatabase routeDatabase() {
return routeDatabase;
}
@@ -296,36 +472,17 @@
* Sets the dispatcher used to set policy and execute asynchronous requests.
* Must not be null.
*/
- public OkHttpClient setDispatcher(Dispatcher dispatcher) {
+ public final OkHttpClient setDispatcher(Dispatcher dispatcher) {
if (dispatcher == null) throw new IllegalArgumentException("dispatcher == null");
this.dispatcher = dispatcher;
return this;
}
- public Dispatcher getDispatcher() {
+ public final Dispatcher getDispatcher() {
return dispatcher;
}
/**
- * @deprecated OkHttp 1.5 enforces an enumeration of {@link Protocol
- * protocols} that can be selected. Please switch to {@link
- * #setProtocols(java.util.List)}.
- */
- @Deprecated
- public OkHttpClient setTransports(List<String> transports) {
- List<Protocol> protocols = new ArrayList<Protocol>(transports.size());
- for (int i = 0, size = transports.size(); i < size; i++) {
- try {
- Protocol protocol = Protocol.find(ByteString.encodeUtf8(transports.get(i)));
- protocols.add(protocol);
- } catch (IOException e) {
- throw new IllegalArgumentException(e);
- }
- }
- return setProtocols(protocols);
- }
-
- /**
* Configure the protocols used by this client to communicate with remote
* servers. By default this client will prefer the most efficient transport
* available, falling back to more ubiquitous protocols. Applications should
@@ -336,28 +493,33 @@
* <ul>
* <li><a href="http://www.w3.org/Protocols/rfc2616/rfc2616.html">http/1.1</a>
* <li><a href="http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1">spdy/3.1</a>
- * <li><a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-09">HTTP-draft-09/2.0</a>
+ * <li><a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16">h2-16</a>
* </ul>
*
* <p><strong>This is an evolving set.</strong> Future releases may drop
- * support for transitional protocols (like spdy/3.1), in favor of their
- * successors (spdy/4 or http/2.0). The http/1.1 transport will never be
- * dropped.
+ * support for transitional protocols (like h2-16), in favor of their
+ * successors (h2). The http/1.1 transport will never be dropped.
*
* <p>If multiple protocols are specified, <a
- * href="https://technotes.googlecode.com/git/nextprotoneg.html">NPN</a> will
- * be used to negotiate a transport. Future releases may use another mechanism
- * (such as <a href="http://tools.ietf.org/html/draft-friedl-tls-applayerprotoneg-02">ALPN</a>)
- * to negotiate a transport.
+ * href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg">ALPN</a>
+ * will be used to negotiate a transport.
+ *
+ * <p>{@link Protocol#HTTP_1_0} is not supported in this set. Requests are
+ * initiated with {@code HTTP/1.1} only. If the server responds with {@code
+ * HTTP/1.0}, that will be exposed by {@link Response#protocol()}.
*
* @param protocols the protocols to use, in order of preference. The list
- * must contain "http/1.1". It must not contain null.
+ * must contain {@link Protocol#HTTP_1_1}. It must not contain null or
+ * {@link Protocol#HTTP_1_0}.
*/
- public OkHttpClient setProtocols(List<Protocol> protocols) {
+ public final OkHttpClient setProtocols(List<Protocol> protocols) {
protocols = Util.immutableList(protocols);
- if (!protocols.contains(Protocol.HTTP_11)) {
+ if (!protocols.contains(Protocol.HTTP_1_1)) {
throw new IllegalArgumentException("protocols doesn't contain http/1.1: " + protocols);
}
+ if (protocols.contains(Protocol.HTTP_1_0)) {
+ throw new IllegalArgumentException("protocols must not contain http/1.0: " + protocols);
+ }
if (protocols.contains(null)) {
throw new IllegalArgumentException("protocols must not contain null");
}
@@ -365,128 +527,65 @@
return this;
}
- /**
- * @deprecated OkHttp 1.5 enforces an enumeration of {@link Protocol
- * protocols} that can be selected. Please switch to {@link
- * #getProtocols()}.
- */
- @Deprecated
- public List<String> getTransports() {
- List<String> transports = new ArrayList<String>(protocols.size());
- for (int i = 0, size = protocols.size(); i < size; i++) {
- transports.add(protocols.get(i).name.utf8());
- }
- return transports;
- }
-
- public List<Protocol> getProtocols() {
+ public final List<Protocol> getProtocols() {
return protocols;
}
- /*
- * Sets the {@code HostResolver} that will be used by this client to resolve
- * hostnames to IP addresses.
- */
- public OkHttpClient setHostResolver(HostResolver hostResolver) {
- this.hostResolver = hostResolver;
+ public final OkHttpClient setConnectionSpecs(List<ConnectionSpec> connectionSpecs) {
+ this.connectionSpecs = Util.immutableList(connectionSpecs);
return this;
}
- public HostResolver getHostResolver() {
- return hostResolver;
+ public final List<ConnectionSpec> getConnectionSpecs() {
+ return connectionSpecs;
}
/**
- * Invokes {@code request} immediately, and blocks until the response can be
- * processed or is in error.
- *
- * <p>The caller may read the response body with the response's
- * {@link Response#body} method. To facilitate connection recycling, callers
- * should always {@link Response.Body#close() close the response body}.
- *
- * <p>Note that transport-layer success (receiving a HTTP response code,
- * headers and body) does not necessarily indicate application-layer
- * success: {@code response} may still indicate an unhappy HTTP response
- * code like 404 or 500.
- *
- * <h3>Non-blocking responses</h3>
- *
- * <p>Receivers do not need to block while waiting for the response body to
- * download. Instead, they can get called back as data arrives. Use {@link
- * Response.Body#ready} to check if bytes should be read immediately. While
- * there is data ready, read it.
- *
- * <p>The current implementation of {@link Response.Body#ready} always
- * returns true when the underlying transport is HTTP/1. This results in
- * blocking on that transport. For effective non-blocking your server must
- * support {@link Protocol#SPDY_3} or {@link Protocol#HTTP_2}.
- *
- * @throws IOException when the request could not be executed due to a
- * connectivity problem or timeout. Because networks can fail during an
- * exchange, it is possible that the remote server accepted the request
- * before the failure.
+ * Returns a modifiable list of interceptors that observe the full span of each call: from before
+ * the connection is established (if any) until after the response source is selected (either the
+ * origin server, cache, or both).
*/
- public Response execute(Request request) throws IOException {
- // Copy the client. Otherwise changes (socket factory, redirect policy,
- // etc.) may incorrectly be reflected in the request when it is executed.
- OkHttpClient client = copyWithDefaults();
- Job job = new Job(dispatcher, client, request, null);
- Response result = job.getResponse(); // Since we don't cancel, this won't be null.
- job.engine.releaseConnection(); // Transfer ownership of the body to the caller.
- return result;
+ public List<Interceptor> interceptors() {
+ return interceptors;
}
/**
- * Schedules {@code request} to be executed at some point in the future. The
- * {@link #getDispatcher dispatcher} defines when the request will run:
- * usually immediately unless there are several other requests currently being
- * executed.
- *
- * <p>This client will later call back {@code responseReceiver} with either an
- * HTTP response or a failure exception. If you {@link #cancel} a request
- * before it completes the receiver will not be called back.
+ * Returns a modifiable list of interceptors that observe a single network request and response.
+ * These interceptors must call {@link Interceptor.Chain#proceed} exactly once: it is an error for
+ * a network interceptor to short-circuit or repeat a network request.
*/
- public void enqueue(Request request, Response.Receiver responseReceiver) {
- dispatcher.enqueue(this, request, responseReceiver);
+ public List<Interceptor> networkInterceptors() {
+ return networkInterceptors;
}
/**
- * Cancels all scheduled tasks tagged with {@code tag}. Requests that are already
- * complete cannot be canceled.
+ * Prepares the {@code request} to be executed at some point in the future.
*/
- public void cancel(Object tag) {
- dispatcher.cancel(tag);
+ public Call newCall(Request request) {
+ return new Call(this, request);
}
- public HttpURLConnection open(URL url) {
- return open(url, proxy);
- }
-
- HttpURLConnection open(URL url, Proxy proxy) {
- String protocol = url.getProtocol();
- OkHttpClient copy = copyWithDefaults();
- copy.proxy = proxy;
-
- if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
- if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
- throw new IllegalArgumentException("Unexpected protocol: " + protocol);
+ /**
+ * Cancels all scheduled or in-flight calls tagged with {@code tag}. Requests
+ * that are already complete cannot be canceled.
+ */
+ public OkHttpClient cancel(Object tag) {
+ getDispatcher().cancel(tag);
+ return this;
}
/**
* Returns a shallow copy of this OkHttpClient that uses the system-wide
* default for each field that hasn't been explicitly configured.
*/
- OkHttpClient copyWithDefaults() {
- OkHttpClient result = clone();
+ final OkHttpClient copyWithDefaults() {
+ OkHttpClient result = new OkHttpClient(this);
if (result.proxySelector == null) {
result.proxySelector = ProxySelector.getDefault();
}
if (result.cookieHandler == null) {
result.cookieHandler = CookieHandler.getDefault();
}
- if (result.responseCache == null) {
- result.responseCache = toOkResponseCache(ResponseCache.getDefault());
- }
if (result.socketFactory == null) {
result.socketFactory = SocketFactory.getDefault();
}
@@ -496,17 +595,23 @@
if (result.hostnameVerifier == null) {
result.hostnameVerifier = OkHostnameVerifier.INSTANCE;
}
+ if (result.certificatePinner == null) {
+ result.certificatePinner = CertificatePinner.DEFAULT;
+ }
if (result.authenticator == null) {
- result.authenticator = HttpAuthenticator.SYSTEM_DEFAULT;
+ result.authenticator = AuthenticatorAdapter.INSTANCE;
}
if (result.connectionPool == null) {
result.connectionPool = ConnectionPool.getDefault();
}
if (result.protocols == null) {
- result.protocols = Protocol.HTTP2_SPDY3_AND_HTTP;
+ result.protocols = DEFAULT_PROTOCOLS;
}
- if (result.hostResolver == null) {
- result.hostResolver = HostResolver.DEFAULT;
+ if (result.connectionSpecs == null) {
+ result.connectionSpecs = DEFAULT_CONNECTION_SPECS;
+ }
+ if (result.network == null) {
+ result.network = Network.DEFAULT;
}
return result;
}
@@ -514,69 +619,33 @@
/**
* Java and Android programs default to using a single global SSL context,
* accessible to HTTP clients as {@link SSLSocketFactory#getDefault()}. If we
- * used the shared SSL context, when OkHttp enables NPN for its SPDY-related
- * stuff, it would also enable NPN for other usages, which might crash them
- * because NPN is enabled when it isn't expected to be.
- * <p>
- * This code avoids that by defaulting to an OkHttp created SSL context. The
- * significant drawback of this approach is that apps that customize the
- * global SSL context will lose these customizations.
+ * used the shared SSL context, when OkHttp enables ALPN for its SPDY-related
+ * stuff, it would also enable ALPN for other usages, which might crash them
+ * because ALPN is enabled when it isn't expected to be.
+ *
+ * <p>This code avoids that by defaulting to an OkHttp-created SSL context.
+ * The drawback of this approach is that apps that customize the global SSL
+ * context will lose these customizations.
*/
private synchronized SSLSocketFactory getDefaultSSLSocketFactory() {
- if (sslSocketFactory == null) {
+ if (defaultSslSocketFactory == null) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, null, null);
- sslSocketFactory = sslContext.getSocketFactory();
+ defaultSslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new AssertionError(); // The system has no TLS. Just give up.
}
}
- return sslSocketFactory;
+ return defaultSslSocketFactory;
}
/** Returns a shallow copy of this OkHttpClient. */
- @Override public OkHttpClient clone() {
+ @Override public final OkHttpClient clone() {
try {
return (OkHttpClient) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
-
- private OkResponseCache toOkResponseCache(ResponseCache responseCache) {
- return responseCache == null || responseCache instanceof OkResponseCache
- ? (OkResponseCache) responseCache
- : new ResponseCacheAdapter(responseCache);
- }
-
- /**
- * Creates a URLStreamHandler as a {@link URL#setURLStreamHandlerFactory}.
- *
- * <p>This code configures OkHttp to handle all HTTP and HTTPS connections
- * created with {@link URL#openConnection()}: <pre> {@code
- *
- * OkHttpClient okHttpClient = new OkHttpClient();
- * URL.setURLStreamHandlerFactory(okHttpClient);
- * }</pre>
- */
- public URLStreamHandler createURLStreamHandler(final String protocol) {
- if (!protocol.equals("http") && !protocol.equals("https")) return null;
-
- return new URLStreamHandler() {
- @Override protected URLConnection openConnection(URL url) {
- return open(url);
- }
-
- @Override protected URLConnection openConnection(URL url, Proxy proxy) {
- return open(url, proxy);
- }
-
- @Override protected int getDefaultPort() {
- if (protocol.equals("http")) return 80;
- if (protocol.equals("https")) return 443;
- throw new AssertionError();
- }
- };
- }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Protocol.java b/okhttp/src/main/java/com/squareup/okhttp/Protocol.java
index e2d7ba9..03093df 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Protocol.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Protocol.java
@@ -15,61 +15,90 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Util;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.List;
-import okio.ByteString;
/**
- * Contains protocols that OkHttp supports
- * <a href="http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04">NPN</a> or
- * <a href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg">ALPN</a> selection.
+ * Protocols that OkHttp implements for <a
+ * href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg">ALPN</a>
+ * selection.
*
- * <p>
* <h3>Protocol vs Scheme</h3>
* Despite its name, {@link java.net.URL#getProtocol()} returns the
- * {@link java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not
- * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word protocol to
- * indicate how HTTP messages are framed.
+ * {@linkplain java.net.URI#getScheme() scheme} (http, https, etc.) of the URL, not
+ * the protocol (http/1.1, spdy/3.1, etc.). OkHttp uses the word <i>protocol</i>
+ * to identify how HTTP messages are framed.
*/
public enum Protocol {
- HTTP_2("HTTP-draft-09/2.0", true),
- SPDY_3("spdy/3.1", true),
- HTTP_11("http/1.1", false);
-
- public static final List<Protocol> HTTP2_SPDY3_AND_HTTP =
- Util.immutableList(Arrays.asList(HTTP_2, SPDY_3, HTTP_11));
- public static final List<Protocol> SPDY3_AND_HTTP11 =
- Util.immutableList(Arrays.asList(SPDY_3, HTTP_11));
- public static final List<Protocol> HTTP2_AND_HTTP_11 =
- Util.immutableList(Arrays.asList(HTTP_2, HTTP_11));
-
- /** Identifier string used in NPN or ALPN selection. */
- public final ByteString name;
+ /**
+ * An obsolete plaintext framing that does not use persistent sockets by
+ * default.
+ */
+ HTTP_1_0("http/1.0"),
/**
- * When true the protocol is binary framed and derived from SPDY.
+ * A plaintext framing that includes persistent connections.
*
- * @see com.squareup.okhttp.internal.spdy.Variant
+ * <p>This version of OkHttp implements <a
+ * href="http://www.ietf.org/rfc/rfc2616.txt">RFC 2616</a>, and tracks
+ * revisions to that spec.
*/
- public final boolean spdyVariant;
+ HTTP_1_1("http/1.1"),
- Protocol(String name, boolean spdyVariant) {
- this.name = ByteString.encodeUtf8(name);
- this.spdyVariant = spdyVariant;
+ /**
+ * Chromium's binary-framed protocol that includes header compression,
+ * multiplexing multiple requests on the same socket, and server-push.
+ * HTTP/1.1 semantics are layered on SPDY/3.
+ *
+ * <p>This version of OkHttp implements SPDY 3 <a
+ * href="http://dev.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1">draft
+ * 3.1</a>. Future releases of OkHttp may use this identifier for a newer draft
+ * of the SPDY spec.
+ */
+ SPDY_3("spdy/3.1"),
+
+ /**
+ * The IETF's binary-framed protocol that includes header compression,
+ * multiplexing multiple requests on the same socket, and server-push.
+ * HTTP/1.1 semantics are layered on HTTP/2.
+ *
+ * <p>This version of OkHttp implements HTTP/2 <a
+ * href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16">draft 16</a>
+ * with HPACK <a
+ * href="http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10">draft
+ * 10</a>. Future releases of OkHttp may use this identifier for a newer draft
+ * of these specs.
+ *
+ * <p>HTTP/2 requires deployments of HTTP/2 that use TLS 1.2 support
+ * {@linkplain com.squareup.okhttp.CipherSuite#TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}
+ * , present in Java 8+ and Android 5+. Servers that enforce this may send an
+ * exception message including the string {@code INADEQUATE_SECURITY}.
+ */
+ HTTP_2("h2-16");
+
+ private final String protocol;
+
+ Protocol(String protocol) {
+ this.protocol = protocol;
}
/**
- * Returns the protocol matching {@code input} or {@link #HTTP_11} is on
- * {@code null}. Throws an {@link IOException} when {@code input} doesn't
- * match the {@link #name} of a supported protocol.
+ * Returns the protocol identified by {@code protocol}.
+ * @throws IOException if {@code protocol} is unknown.
*/
- public static Protocol find(ByteString input) throws IOException {
- if (input == null) return HTTP_11;
- for (Protocol protocol : values()) {
- if (protocol.name.equals(input)) return protocol;
- }
- throw new IOException("Unexpected protocol: " + input.utf8());
+ public static Protocol get(String protocol) throws IOException {
+ // Unroll the loop over values() to save an allocation.
+ if (protocol.equals(HTTP_1_0.protocol)) return HTTP_1_0;
+ if (protocol.equals(HTTP_1_1.protocol)) return HTTP_1_1;
+ if (protocol.equals(HTTP_2.protocol)) return HTTP_2;
+ if (protocol.equals(SPDY_3.protocol)) return SPDY_3;
+ throw new IOException("Unexpected protocol: " + protocol);
+ }
+
+ /**
+ * Returns the string used to identify this protocol for ALPN, like
+ * "http/1.1", "spdy/3.1" or "h2-16".
+ */
+ @Override public String toString() {
+ return protocol;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index 300dc17..c40ada3 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -17,56 +17,58 @@
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
-import java.io.File;
-import java.io.FileInputStream;
+import com.squareup.okhttp.internal.http.HttpMethod;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
-import okio.BufferedSink;
/**
* An HTTP request. Instances of this class are immutable if their {@link #body}
* is null or itself immutable.
*/
public final class Request {
- private final URL url;
+ private final String urlString;
private final String method;
private final Headers headers;
- private final Body body;
+ private final RequestBody body;
private final Object tag;
- private volatile ParsedHeaders parsedHeaders; // Lazily initialized.
+ private volatile URL url; // Lazily initialized.
private volatile URI uri; // Lazily initialized.
private volatile CacheControl cacheControl; // Lazily initialized.
private Request(Builder builder) {
- this.url = builder.url;
+ this.urlString = builder.urlString;
this.method = builder.method;
this.headers = builder.headers.build();
this.body = builder.body;
this.tag = builder.tag != null ? builder.tag : this;
+ this.url = builder.url;
}
public URL url() {
- return url;
+ try {
+ URL result = url;
+ return result != null ? result : (url = new URL(urlString));
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("Malformed URL: " + urlString, e);
+ }
}
public URI uri() throws IOException {
try {
URI result = uri;
- return result != null ? result : (uri = Platform.get().toUriLenient(url));
+ return result != null ? result : (uri = Platform.get().toUriLenient(url()));
} catch (URISyntaxException e) {
throw new IOException(e.getMessage());
}
}
public String urlString() {
- return url.toString();
+ return urlString;
}
public String method() {
@@ -85,7 +87,7 @@
return headers.values(name);
}
- public Body body() {
+ public RequestBody body() {
return body;
}
@@ -97,23 +99,6 @@
return new Builder(this);
}
- public Headers getHeaders() {
- return headers;
- }
-
- public String getUserAgent() {
- return parsedHeaders().userAgent;
- }
-
- public String getProxyAuthorization() {
- return parsedHeaders().proxyAuthorization;
- }
-
- private ParsedHeaders parsedHeaders() {
- ParsedHeaders result = parsedHeaders;
- return result != null ? result : (parsedHeaders = new ParsedHeaders(headers));
- }
-
/**
* Returns the cache control directives for this response. This is never null,
* even if this response contains no {@code Cache-Control} header.
@@ -127,113 +112,22 @@
return url().getProtocol().equals("https");
}
- /** Parsed request headers, computed on-demand and cached. */
- private static class ParsedHeaders {
- private String userAgent;
- private String proxyAuthorization;
-
- public ParsedHeaders(Headers headers) {
- for (int i = 0; i < headers.size(); i++) {
- String fieldName = headers.name(i);
- String value = headers.value(i);
- if ("User-Agent".equalsIgnoreCase(fieldName)) {
- userAgent = value;
- } else if ("Proxy-Authorization".equalsIgnoreCase(fieldName)) {
- proxyAuthorization = value;
- }
- }
- }
- }
-
- public abstract static class Body {
- /** Returns the Content-Type header for this body. */
- public abstract MediaType contentType();
-
- /**
- * Returns the number of bytes that will be written to {@code out} in a call
- * to {@link #writeTo}, or -1 if that count is unknown.
- */
- public long contentLength() {
- return -1;
- }
-
- /** Writes the content of this request to {@code out}. */
- public abstract void writeTo(BufferedSink sink) throws IOException;
-
- /**
- * Returns a new request body that transmits {@code content}. If {@code
- * contentType} lacks a charset, this will use UTF-8.
- */
- public static Body create(MediaType contentType, String content) {
- contentType = contentType.charset() != null
- ? contentType
- : MediaType.parse(contentType + "; charset=utf-8");
- try {
- byte[] bytes = content.getBytes(contentType.charset().name());
- return create(contentType, bytes);
- } catch (UnsupportedEncodingException e) {
- throw new AssertionError();
- }
- }
-
- /** Returns a new request body that transmits {@code content}. */
- public static Body create(final MediaType contentType, final byte[] content) {
- if (contentType == null) throw new NullPointerException("contentType == null");
- if (content == null) throw new NullPointerException("content == null");
-
- return new Body() {
- @Override public MediaType contentType() {
- return contentType;
- }
-
- @Override public long contentLength() {
- return content.length;
- }
-
- @Override public void writeTo(BufferedSink sink) throws IOException {
- sink.write(content);
- }
- };
- }
-
- /** Returns a new request body that transmits the content of {@code file}. */
- public static Body create(final MediaType contentType, final File file) {
- if (contentType == null) throw new NullPointerException("contentType == null");
- if (file == null) throw new NullPointerException("content == null");
-
- return new Body() {
- @Override public MediaType contentType() {
- return contentType;
- }
-
- @Override public long contentLength() {
- return file.length();
- }
-
- @Override public void writeTo(BufferedSink sink) throws IOException {
- long length = contentLength();
- if (length == 0) return;
-
- InputStream in = null;
- try {
- in = new FileInputStream(file);
- byte[] buffer = new byte[(int) Math.min(8192, length)];
- for (int c; (c = in.read(buffer)) != -1; ) {
- sink.write(buffer, 0, c);
- }
- } finally {
- Util.closeQuietly(in);
- }
- }
- };
- }
+ @Override public String toString() {
+ return "Request{method="
+ + method
+ + ", url="
+ + urlString
+ + ", tag="
+ + (tag != this ? tag : null)
+ + '}';
}
public static class Builder {
+ private String urlString;
private URL url;
private String method;
private Headers.Builder headers;
- private Body body;
+ private RequestBody body;
private Object tag;
public Builder() {
@@ -242,6 +136,7 @@
}
private Builder(Request request) {
+ this.urlString = request.urlString;
this.url = request.url;
this.method = request.method;
this.body = request.body;
@@ -250,16 +145,15 @@
}
public Builder url(String url) {
- try {
- return url(new URL(url));
- } catch (MalformedURLException e) {
- throw new IllegalArgumentException("Malformed URL: " + url);
- }
+ if (url == null) throw new IllegalArgumentException("url == null");
+ urlString = url;
+ return this;
}
public Builder url(URL url) {
if (url == null) throw new IllegalArgumentException("url == null");
this.url = url;
+ this.urlString = url.toString();
return this;
}
@@ -292,8 +186,15 @@
return this;
}
- public Builder setUserAgent(String userAgent) {
- return header("User-Agent", userAgent);
+ /**
+ * Sets this request's {@code Cache-Control} header, replacing any cache
+ * control headers already present. If {@code cacheControl} doesn't define
+ * any directives, this clears this request's cache-control headers.
+ */
+ public Builder cacheControl(CacheControl cacheControl) {
+ String value = cacheControl.toString();
+ if (value.isEmpty()) return removeHeader("Cache-Control");
+ return header("Cache-Control", value);
}
public Builder get() {
@@ -304,18 +205,36 @@
return method("HEAD", null);
}
- public Builder post(Body body) {
+ public Builder post(RequestBody body) {
return method("POST", body);
}
- public Builder put(Body body) {
+ public Builder delete(RequestBody body) {
+ return method("DELETE", body);
+ }
+
+ public Builder delete() {
+ return method("DELETE", null);
+ }
+
+ public Builder put(RequestBody body) {
return method("PUT", body);
}
- public Builder method(String method, Body body) {
+ public Builder patch(RequestBody body) {
+ return method("PATCH", body);
+ }
+
+ public Builder method(String method, RequestBody body) {
if (method == null || method.length() == 0) {
throw new IllegalArgumentException("method == null || method.length() == 0");
}
+ if (body != null && !HttpMethod.permitsRequestBody(method)) {
+ throw new IllegalArgumentException("method " + method + " must not have a request body.");
+ }
+ if (body == null && HttpMethod.permitsRequestBody(method)) {
+ body = RequestBody.create(null, Util.EMPTY_BYTE_ARRAY);
+ }
this.method = method;
this.body = body;
return this;
@@ -332,7 +251,7 @@
}
public Request build() {
- if (url == null) throw new IllegalStateException("url == null");
+ if (urlString == null) throw new IllegalStateException("url == null");
return new Request(this);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/RequestBody.java b/okhttp/src/main/java/com/squareup/okhttp/RequestBody.java
new file mode 100644
index 0000000..5d74837
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/RequestBody.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 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.Util;
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import okio.BufferedSink;
+import okio.Okio;
+import okio.Source;
+
+public abstract class RequestBody {
+ /** Returns the Content-Type header for this body. */
+ public abstract MediaType contentType();
+
+ /**
+ * Returns the number of bytes that will be written to {@code out} in a call
+ * to {@link #writeTo}, or -1 if that count is unknown.
+ */
+ public long contentLength() throws IOException {
+ return -1;
+ }
+
+ /** Writes the content of this request to {@code out}. */
+ public abstract void writeTo(BufferedSink sink) throws IOException;
+
+ /**
+ * Returns a new request body that transmits {@code content}. If {@code
+ * contentType} is non-null and lacks a charset, this will use UTF-8.
+ */
+ public static RequestBody create(MediaType contentType, String content) {
+ Charset charset = Util.UTF_8;
+ if (contentType != null) {
+ charset = contentType.charset();
+ if (charset == null) {
+ charset = Util.UTF_8;
+ contentType = MediaType.parse(contentType + "; charset=utf-8");
+ }
+ }
+ byte[] bytes = content.getBytes(charset);
+ return create(contentType, bytes);
+ }
+
+ /** Returns a new request body that transmits {@code content}. */
+ public static RequestBody create(final MediaType contentType, final byte[] content) {
+ if (content == null) throw new NullPointerException("content == null");
+
+ return new RequestBody() {
+ @Override public MediaType contentType() {
+ return contentType;
+ }
+
+ @Override public long contentLength() {
+ return content.length;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.write(content);
+ }
+ };
+ }
+
+ /** Returns a new request body that transmits the content of {@code file}. */
+ public static RequestBody create(final MediaType contentType, final File file) {
+ if (file == null) throw new NullPointerException("content == null");
+
+ return new RequestBody() {
+ @Override public MediaType contentType() {
+ return contentType;
+ }
+
+ @Override public long contentLength() {
+ return file.length();
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ Source source = null;
+ try {
+ source = Okio.source(file);
+ sink.writeAll(source);
+ } finally {
+ Util.closeQuietly(source);
+ }
+ }
+ };
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Response.java b/okhttp/src/main/java/com/squareup/okhttp/Response.java
index 03c2d1f..bf52795 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Response.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Response.java
@@ -15,28 +15,18 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.HttpDate;
import com.squareup.okhttp.internal.http.OkHeaders;
-import com.squareup.okhttp.internal.http.StatusLine;
-import java.io.ByteArrayOutputStream;
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.net.HttpURLConnection;
-import java.nio.charset.Charset;
import java.util.Collections;
-import java.util.Date;
import java.util.List;
-import java.util.Set;
-import java.util.TreeSet;
-import okio.Okio;
-import okio.Source;
-import static com.squareup.okhttp.internal.Util.UTF_8;
-import static com.squareup.okhttp.internal.Util.equal;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
/**
* An HTTP response. Instances of this class are not immutable: the response
@@ -45,20 +35,23 @@
*/
public final class Response {
private final Request request;
- private final StatusLine statusLine;
+ private final Protocol protocol;
+ private final int code;
+ private final String message;
private final Handshake handshake;
private final Headers headers;
- private final Body body;
+ private final ResponseBody body;
private Response networkResponse;
private Response cacheResponse;
private final Response priorResponse;
- private volatile ParsedHeaders parsedHeaders; // Lazily initialized.
private volatile CacheControl cacheControl; // Lazily initialized.
private Response(Builder builder) {
this.request = builder.request;
- this.statusLine = builder.statusLine;
+ this.protocol = builder.protocol;
+ this.code = builder.code;
+ this.message = builder.message;
this.handshake = builder.handshake;
this.headers = builder.headers.build();
this.body = builder.body;
@@ -68,35 +61,44 @@
}
/**
- * The wire-level request that initiated this HTTP response. This is usually
- * <strong>not</strong> the same request instance provided to the HTTP client:
+ * The wire-level request that initiated this HTTP response. This is not
+ * necessarily the same request issued by the application:
* <ul>
* <li>It may be transformed by the HTTP client. For example, the client
- * may have added its own {@code Content-Encoding} header to enable
- * response compression.
- * <li>It may be the request generated in response to an HTTP redirect.
- * In this case the request URL may be different than the initial
- * request URL.
+ * may copy headers like {@code Content-Length} from the request body.
+ * <li>It may be the request generated in response to an HTTP redirect or
+ * authentication challenge. In this case the request URL may be
+ * different than the initial request URL.
* </ul>
*/
public Request request() {
return request;
}
- public String statusLine() {
- return statusLine.getStatusLine();
+ /**
+ * Returns the HTTP protocol, such as {@link Protocol#HTTP_1_1} or {@link
+ * Protocol#HTTP_1_0}.
+ */
+ public Protocol protocol() {
+ return protocol;
}
+ /** Returns the HTTP status code. */
public int code() {
- return statusLine.code();
+ return code;
}
- public String statusMessage() {
- return statusLine.message();
+ /**
+ * Returns true if the code is in [200..300), which means the request was
+ * successfully received, understood, and accepted.
+ */
+ public boolean isSuccessful() {
+ return code >= 200 && code < 300;
}
- public int httpMinorVersion() {
- return statusLine.httpMinorVersion();
+ /** Returns the HTTP status message or null if it is unknown. */
+ public String message() {
+ return message;
}
/**
@@ -124,7 +126,7 @@
return headers;
}
- public Body body() {
+ public ResponseBody body() {
return body;
}
@@ -132,14 +134,19 @@
return new Builder(this);
}
- /**
- * Returns the response for the HTTP redirect that triggered this response, or
- * null if this response wasn't triggered by an automatic redirect. The body
- * of the returned response should not be read because it has already been
- * consumed by the redirecting client.
- */
- public Response priorResponse() {
- return priorResponse;
+ /** Returns true if this response redirects to another resource. */
+ public boolean isRedirect() {
+ switch (code) {
+ case HTTP_PERM_REDIRECT:
+ case HTTP_TEMP_REDIRECT:
+ case HTTP_MULT_CHOICE:
+ case HTTP_MOVED_PERM:
+ case HTTP_MOVED_TEMP:
+ case HTTP_SEE_OTHER:
+ return true;
+ default:
+ return false;
+ }
}
/**
@@ -161,144 +168,33 @@
return cacheResponse;
}
- // TODO: move out of public API
- public Set<String> getVaryFields() {
- return parsedHeaders().varyFields;
+ /**
+ * Returns the response for the HTTP redirect or authorization challenge that
+ * triggered this response, or null if this response wasn't triggered by an
+ * automatic retry. The body of the returned response should not be read
+ * because it has already been consumed by the redirecting client.
+ */
+ public Response priorResponse() {
+ return priorResponse;
}
/**
- * Returns true if a Vary header contains an asterisk. Such responses cannot
- * be cached.
+ * Returns the authorization challenges appropriate for this response's code.
+ * If the response code is 401 unauthorized, this returns the
+ * "WWW-Authenticate" challenges. If the response code is 407 proxy
+ * unauthorized, this returns the "Proxy-Authenticate" challenges. Otherwise
+ * this returns an empty list of challenges.
*/
- // TODO: move out of public API
- public boolean hasVaryAll() {
- return parsedHeaders().varyFields.contains("*");
- }
-
- /**
- * Returns true if none of the Vary headers on this response have changed
- * between {@code cachedRequest} and {@code newRequest}.
- */
- // TODO: move out of public API
- public boolean varyMatches(Headers varyHeaders, Request newRequest) {
- for (String field : parsedHeaders().varyFields) {
- if (!equal(varyHeaders.values(field), newRequest.headers(field))) return false;
+ public List<Challenge> challenges() {
+ String responseField;
+ if (code == HTTP_UNAUTHORIZED) {
+ responseField = "WWW-Authenticate";
+ } else if (code == HTTP_PROXY_AUTH) {
+ responseField = "Proxy-Authenticate";
+ } else {
+ return Collections.emptyList();
}
- return true;
- }
-
- /**
- * Returns true if this cached response should be used; false if the
- * network response should be used.
- */
- // TODO: move out of public API
- public boolean validate(Response network) {
- if (network.code() == HttpURLConnection.HTTP_NOT_MODIFIED) {
- return true;
- }
-
- // The HTTP spec says that if the network's response is older than our
- // cached response, we may return the cache's response. Like Chrome (but
- // unlike Firefox), this client prefers to return the newer response.
- ParsedHeaders networkHeaders = network.parsedHeaders();
- if (parsedHeaders().lastModified != null
- && networkHeaders.lastModified != null
- && networkHeaders.lastModified.getTime() < parsedHeaders().lastModified.getTime()) {
- return true;
- }
-
- return false;
- }
-
- public abstract static class Body implements Closeable {
- /** Multiple calls to {@link #charStream()} must return the same instance. */
- private Reader reader;
-
- /** Multiple calls to {@link #source()} must return the same instance. */
- private Source source;
-
- /**
- * Returns true if further data from this response body should be read at
- * this time. For asynchronous protocols like SPDY and HTTP/2, this will
- * return false once all locally-available body bytes have been read.
- *
- * <p>Clients with many concurrent downloads can use this method to reduce
- * the number of idle threads blocking on reads. See {@link
- * Receiver#onResponse} for details.
- */
- // <h3>Body.ready() vs. InputStream.available()</h3>
- // TODO: Can we fix response bodies to implement InputStream.available well?
- // The deflater implementation is broken by default but we could do better.
- public abstract boolean ready() throws IOException;
-
- public abstract MediaType contentType();
-
- /**
- * Returns the number of bytes in that will returned by {@link #bytes}, or
- * {@link #byteStream}, or -1 if unknown.
- */
- public abstract long contentLength();
-
- public abstract InputStream byteStream();
-
- // TODO: Source needs to be an API type for this to be public
- public Source source() {
- Source s = source;
- return s != null ? s : (source = Okio.source(byteStream()));
- }
-
- public final byte[] bytes() throws IOException {
- long contentLength = contentLength();
- if (contentLength > Integer.MAX_VALUE) {
- throw new IOException("Cannot buffer entire body for content length: " + contentLength);
- }
-
- if (contentLength != -1) {
- byte[] content = new byte[(int) contentLength];
- InputStream in = byteStream();
- Util.readFully(in, content);
- if (in.read() != -1) throw new IOException("Content-Length and stream length disagree");
- return content;
-
- } else {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- Util.copy(byteStream(), out);
- return out.toByteArray();
- }
- }
-
- /**
- * Returns the response as a character stream decoded with the charset
- * of the Content-Type header. If that header is either absent or lacks a
- * charset, this will attempt to decode the response body as UTF-8.
- */
- public final Reader charStream() {
- Reader r = reader;
- return r != null ? r : (reader = new InputStreamReader(byteStream(), charset()));
- }
-
- /**
- * Returns the response as a string decoded with the charset of the
- * Content-Type header. If that header is either absent or lacks a charset,
- * this will attempt to decode the response body as UTF-8.
- */
- public final String string() throws IOException {
- return new String(bytes(), charset().name());
- }
-
- private Charset charset() {
- MediaType contentType = contentType();
- return contentType != null ? contentType.charset(UTF_8) : UTF_8;
- }
-
- @Override public void close() throws IOException {
- byteStream().close();
- }
- }
-
- private ParsedHeaders parsedHeaders() {
- ParsedHeaders result = parsedHeaders;
- return result != null ? result : (parsedHeaders = new ParsedHeaders(headers));
+ return OkHeaders.parseChallenges(headers(), responseField);
}
/**
@@ -310,80 +206,26 @@
return result != null ? result : (cacheControl = CacheControl.parse(headers));
}
- /** Parsed response headers, computed on-demand and cached. */
- private static class ParsedHeaders {
- /** The last modified date of the response, if known. */
- Date lastModified;
-
- /** Case-insensitive set of field names. */
- private Set<String> varyFields = Collections.emptySet();
-
- private ParsedHeaders(Headers headers) {
- for (int i = 0; i < headers.size(); i++) {
- String fieldName = headers.name(i);
- String value = headers.value(i);
- if ("Last-Modified".equalsIgnoreCase(fieldName)) {
- lastModified = HttpDate.parse(value);
- } else if ("Vary".equalsIgnoreCase(fieldName)) {
- // Replace the immutable empty set with something we can mutate.
- if (varyFields.isEmpty()) {
- varyFields = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
- }
- for (String varyField : value.split(",")) {
- varyFields.add(varyField.trim());
- }
- }
- }
- }
- }
-
- public interface Receiver {
- /**
- * Called when the request could not be executed due to a connectivity
- * problem or timeout. Because networks can fail during an exchange, it is
- * possible that the remote server accepted the request before the failure.
- */
- void onFailure(Failure failure);
-
- /**
- * Called when the HTTP response was successfully returned by the remote
- * server. The receiver may proceed to read the response body with the
- * response's {@link #body} method.
- *
- * <p>Note that transport-layer success (receiving a HTTP response code,
- * headers and body) does not necessarily indicate application-layer
- * success: {@code response} may still indicate an unhappy HTTP response
- * code like 404 or 500.
- *
- * <h3>Non-blocking responses</h3>
- *
- * <p>Receivers do not need to block while waiting for the response body to
- * download. Instead, they can get called back as data arrives. Use {@link
- * Body#ready} to check if bytes should be read immediately. While there is
- * data ready, read it. If there isn't, return false: receivers will be
- * called back with {@code onResponse()} as additional data is downloaded.
- *
- * <p>Return true to indicate that the receiver has finished handling the
- * response body. If the response body has unread data, it will be
- * discarded.
- *
- * <p>When the response body has been fully consumed the returned value is
- * undefined.
- *
- * <p>The current implementation of {@link Body#ready} always returns true
- * when the underlying transport is HTTP/1. This results in blocking on that
- * transport. For effective non-blocking your server must support SPDY or
- * HTTP/2.
- */
- boolean onResponse(Response response) throws IOException;
+ @Override public String toString() {
+ return "Response{protocol="
+ + protocol
+ + ", code="
+ + code
+ + ", message="
+ + message
+ + ", url="
+ + request.urlString()
+ + '}';
}
public static class Builder {
private Request request;
- private StatusLine statusLine;
+ private Protocol protocol;
+ private int code = -1;
+ private String message;
private Handshake handshake;
private Headers.Builder headers;
- private Body body;
+ private ResponseBody body;
private Response networkResponse;
private Response cacheResponse;
private Response priorResponse;
@@ -394,7 +236,9 @@
private Builder(Response response) {
this.request = response.request;
- this.statusLine = response.statusLine;
+ this.protocol = response.protocol;
+ this.code = response.code;
+ this.message = response.message;
this.handshake = response.handshake;
this.headers = response.headers.newBuilder();
this.body = response.body;
@@ -408,18 +252,19 @@
return this;
}
- public Builder statusLine(StatusLine statusLine) {
- if (statusLine == null) throw new IllegalArgumentException("statusLine == null");
- this.statusLine = statusLine;
+ public Builder protocol(Protocol protocol) {
+ this.protocol = protocol;
return this;
}
- public Builder statusLine(String statusLine) {
- try {
- return statusLine(new StatusLine(statusLine));
- } catch (IOException e) {
- throw new IllegalArgumentException(e);
- }
+ public Builder code(int code) {
+ this.code = code;
+ return this;
+ }
+
+ public Builder message(String message) {
+ this.message = message;
+ return this;
}
public Builder handshake(Handshake handshake) {
@@ -456,16 +301,11 @@
return this;
}
- public Builder body(Body body) {
+ public Builder body(ResponseBody body) {
this.body = body;
return this;
}
- // TODO: move out of public API
- public Builder setResponseSource(ResponseSource responseSource) {
- return header(OkHeaders.RESPONSE_SOURCE, responseSource + " " + statusLine.code());
- }
-
public Builder networkResponse(Response networkResponse) {
if (networkResponse != null) checkSupportResponse("networkResponse", networkResponse);
this.networkResponse = networkResponse;
@@ -491,13 +331,21 @@
}
public Builder priorResponse(Response priorResponse) {
+ if (priorResponse != null) checkPriorResponse(priorResponse);
this.priorResponse = priorResponse;
return this;
}
+ private void checkPriorResponse(Response response) {
+ if (response.body != null) {
+ throw new IllegalArgumentException("priorResponse.body != null");
+ }
+ }
+
public Response build() {
if (request == null) throw new IllegalStateException("request == null");
- if (statusLine == null) throw new IllegalStateException("statusLine == null");
+ if (protocol == null) throw new IllegalStateException("protocol == null");
+ if (code < 0) throw new IllegalStateException("code < 0: " + code);
return new Response(this);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ResponseBody.java b/okhttp/src/main/java/com/squareup/okhttp/ResponseBody.java
new file mode 100644
index 0000000..bdd98b4
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/ResponseBody.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2014 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.Util;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import okio.Buffer;
+import okio.BufferedSource;
+
+import static com.squareup.okhttp.internal.Util.UTF_8;
+
+public abstract class ResponseBody implements Closeable {
+ /** Multiple calls to {@link #charStream()} must return the same instance. */
+ private Reader reader;
+
+ public abstract MediaType contentType();
+
+ /**
+ * Returns the number of bytes in that will returned by {@link #bytes}, or
+ * {@link #byteStream}, or -1 if unknown.
+ */
+ public abstract long contentLength() throws IOException;
+
+ public final InputStream byteStream() throws IOException {
+ return source().inputStream();
+ }
+
+ public abstract BufferedSource source() throws IOException;
+
+ public final byte[] bytes() throws IOException {
+ long contentLength = contentLength();
+ if (contentLength > Integer.MAX_VALUE) {
+ throw new IOException("Cannot buffer entire body for content length: " + contentLength);
+ }
+
+ BufferedSource source = source();
+ byte[] bytes;
+ try {
+ bytes = source.readByteArray();
+ } finally {
+ Util.closeQuietly(source);
+ }
+ if (contentLength != -1 && contentLength != bytes.length) {
+ throw new IOException("Content-Length and stream length disagree");
+ }
+ return bytes;
+ }
+
+ /**
+ * Returns the response as a character stream decoded with the charset
+ * of the Content-Type header. If that header is either absent or lacks a
+ * charset, this will attempt to decode the response body as UTF-8.
+ */
+ public final Reader charStream() throws IOException {
+ Reader r = reader;
+ return r != null ? r : (reader = new InputStreamReader(byteStream(), charset()));
+ }
+
+ /**
+ * Returns the response as a string decoded with the charset of the
+ * Content-Type header. If that header is either absent or lacks a charset,
+ * this will attempt to decode the response body as UTF-8.
+ */
+ public final String string() throws IOException {
+ return new String(bytes(), charset().name());
+ }
+
+ private Charset charset() {
+ MediaType contentType = contentType();
+ return contentType != null ? contentType.charset(UTF_8) : UTF_8;
+ }
+
+ @Override public void close() throws IOException {
+ source().close();
+ }
+
+ /**
+ * Returns a new response body that transmits {@code content}. If {@code
+ * contentType} is non-null and lacks a charset, this will use UTF-8.
+ */
+ public static ResponseBody create(MediaType contentType, String content) {
+ Charset charset = Util.UTF_8;
+ if (contentType != null) {
+ charset = contentType.charset();
+ if (charset == null) {
+ charset = Util.UTF_8;
+ contentType = MediaType.parse(contentType + "; charset=utf-8");
+ }
+ }
+ Buffer buffer = new Buffer().writeString(content, charset);
+ return create(contentType, buffer.size(), buffer);
+ }
+
+ /** Returns a new response body that transmits {@code content}. */
+ public static ResponseBody create(final MediaType contentType, byte[] content) {
+ Buffer buffer = new Buffer().write(content);
+ return create(contentType, content.length, buffer);
+ }
+
+ /** Returns a new response body that transmits {@code content}. */
+ public static ResponseBody create(
+ final MediaType contentType, final long contentLength, final BufferedSource content) {
+ if (content == null) throw new NullPointerException("source == null");
+ return new ResponseBody() {
+ @Override public MediaType contentType() {
+ return contentType;
+ }
+
+ @Override public long contentLength() {
+ return contentLength;
+ }
+
+ @Override public BufferedSource source() {
+ return content;
+ }
+ };
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java b/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java
deleted file mode 100644
index 915fa58..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/ResponseSource.java
+++ /dev/null
@@ -1,48 +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;
-
-/** The source of an HTTP response. */
-public enum ResponseSource {
-
- /** The response was returned from the local cache. */
- CACHE,
-
- /**
- * The response is available in the cache but must be validated with the
- * network. The cache result will be used if it is still valid; otherwise
- * the network's response will be used.
- */
- CONDITIONAL_CACHE,
-
- /** The response was returned from the network. */
- NETWORK,
-
- /**
- * The request demanded a cached response that the cache couldn't satisfy.
- * This yields a 504 (Gateway Timeout) response as specified by
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4.
- */
- NONE;
-
- public boolean requiresConnection() {
- return this == CONDITIONAL_CACHE || this == NETWORK;
- }
-
- public boolean usesCache() {
- return this == CACHE || this == CONDITIONAL_CACHE;
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Route.java b/okhttp/src/main/java/com/squareup/okhttp/Route.java
index 4f99075..f244311 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Route.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Route.java
@@ -23,29 +23,50 @@
* When creating a connection the client has many options:
* <ul>
* <li><strong>HTTP proxy:</strong> a proxy server may be explicitly
- * configured for the client. Otherwise the {@link java.net.ProxySelector
+ * configured for the client. Otherwise the {@linkplain java.net.ProxySelector
* proxy selector} is used. It may return multiple proxies to attempt.
* <li><strong>IP address:</strong> whether connecting directly to an origin
* server or a proxy, opening a socket requires an IP address. The DNS
* server may return multiple IP addresses to attempt.
+ * <li><strong>TLS configuration:</strong> which cipher suites and TLS
+ * versions to attempt with the HTTPS connection.
* </ul>
* Each route is a specific selection of these options.
*/
-public class Route {
+public final class Route {
final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
+ final ConnectionSpec connectionSpec;
+ final boolean shouldSendTlsFallbackIndicator;
- public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {
- if (address == null) throw new NullPointerException("address == null");
- if (proxy == null) throw new NullPointerException("proxy == null");
- if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
+ public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+ ConnectionSpec connectionSpec) {
+ this(address, proxy, inetSocketAddress, connectionSpec,
+ false /* shouldSendTlsFallbackIndicator */);
+ }
+
+ public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
+ ConnectionSpec connectionSpec, boolean shouldSendTlsFallbackIndicator) {
+ if (address == null) {
+ throw new NullPointerException("address == null");
+ }
+ if (proxy == null) {
+ throw new NullPointerException("proxy == null");
+ }
+ if (inetSocketAddress == null) {
+ throw new NullPointerException("inetSocketAddress == null");
+ }
+ if (connectionSpec == null) {
+ throw new NullPointerException("connectionConfiguration == null");
+ }
this.address = address;
this.proxy = proxy;
this.inetSocketAddress = inetSocketAddress;
+ this.connectionSpec = connectionSpec;
+ this.shouldSendTlsFallbackIndicator = shouldSendTlsFallbackIndicator;
}
- /** Returns the {@link Address} of this route. */
public Address getAddress() {
return address;
}
@@ -54,23 +75,41 @@
* Returns the {@link Proxy} of this route.
*
* <strong>Warning:</strong> This may disagree with {@link Address#getProxy}
- * is null. When the address's proxy is null, the proxy selector will be used.
+ * when it is null. When the address's proxy is null, the proxy selector is
+ * used.
*/
public Proxy getProxy() {
return proxy;
}
- /** Returns the {@link InetSocketAddress} of this route. */
public InetSocketAddress getSocketAddress() {
return inetSocketAddress;
}
+ public ConnectionSpec getConnectionSpec() {
+ return connectionSpec;
+ }
+
+ public boolean getShouldSendTlsFallbackIndicator() {
+ return shouldSendTlsFallbackIndicator;
+ }
+
+ /**
+ * Returns true if this route tunnels HTTPS through an HTTP proxy. See <a
+ * href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section 5.2</a>.
+ */
+ public boolean requiresTunnel() {
+ return address.sslSocketFactory != null && proxy.type() == Proxy.Type.HTTP;
+ }
+
@Override public boolean equals(Object obj) {
if (obj instanceof Route) {
Route other = (Route) obj;
- return (address.equals(other.address)
+ return address.equals(other.address)
&& proxy.equals(other.proxy)
- && inetSocketAddress.equals(other.inetSocketAddress));
+ && inetSocketAddress.equals(other.inetSocketAddress)
+ && connectionSpec.equals(other.connectionSpec)
+ && shouldSendTlsFallbackIndicator == other.shouldSendTlsFallbackIndicator;
}
return false;
}
@@ -80,6 +119,8 @@
result = 31 * result + address.hashCode();
result = 31 * result + proxy.hashCode();
result = 31 * result + inetSocketAddress.hashCode();
+ result = 31 * result + connectionSpec.hashCode();
+ result = 31 * result + (shouldSendTlsFallbackIndicator ? 1 : 0);
return result;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
new file mode 100644
index 0000000..a8d7b9b
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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 javax.net.ssl.SSLSocket;
+
+/**
+ * Versions of TLS that can be offered when negotiating a secure socket. See
+ * {@link SSLSocket#setEnabledProtocols}.
+ */
+public enum TlsVersion {
+ TLS_1_2("TLSv1.2"), // 2008.
+ TLS_1_1("TLSv1.1"), // 2006.
+ TLS_1_0("TLSv1"), // 1999.
+ SSL_3_0("SSLv3"), // 1996.
+ ;
+
+ final String javaName;
+
+ private TlsVersion(String javaName) {
+ this.javaName = javaName;
+ }
+
+ public static TlsVersion forJavaName(String javaName) {
+ switch (javaName) {
+ case "TLSv1.2": return TLS_1_2;
+ case "TLSv1.1": return TLS_1_1;
+ case "TLSv1": return TLS_1_0;
+ case "SSLv3": return SSL_3_0;
+ }
+ throw new IllegalArgumentException("Unexpected TLS version: " + javaName);
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java b/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java
deleted file mode 100644
index 3bcff5a..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/TunnelRequest.java
+++ /dev/null
@@ -1,80 +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;
-
-import java.io.IOException;
-import java.net.URL;
-
-import static com.squareup.okhttp.internal.Util.getDefaultPort;
-
-/**
- * Routing and authentication information sent to an HTTP proxy to create a
- * HTTPS to an origin server. Everything in the tunnel request is sent
- * unencrypted to the proxy server.
- *
- * <p>See <a href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section
- * 5.2</a>.
- */
-public final class TunnelRequest {
- final String host;
- final int port;
- final String userAgent;
- final String proxyAuthorization;
-
- /**
- * @param host the origin server's hostname. Not null.
- * @param port the origin server's port, like 80 or 443.
- * @param userAgent the client's user-agent. Not null.
- * @param proxyAuthorization proxy authorization, or null if the proxy is
- * used without an authorization header.
- */
- public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) {
- if (host == null) throw new NullPointerException("host == null");
- if (userAgent == null) throw new NullPointerException("userAgent == null");
- this.host = host;
- this.port = port;
- this.userAgent = userAgent;
- this.proxyAuthorization = proxyAuthorization;
- }
-
- String requestLine() {
- return "CONNECT " + host + ":" + port + " HTTP/1.1";
- }
-
- /**
- * If we're creating a TLS tunnel, send only the minimum set of headers.
- * This avoids sending potentially sensitive data like HTTP cookies to
- * the proxy unencrypted.
- */
- Request getRequest() throws IOException {
- Request.Builder result = new Request.Builder()
- .url(new URL("https", host, port, "/"));
-
- // Always set Host and User-Agent.
- result.header("Host", port == getDefaultPort("https") ? host : (host + ":" + port));
- result.header("User-Agent", userAgent);
-
- // Copy over the Proxy-Authorization header if it exists.
- if (proxyAuthorization != null) {
- result.header("Proxy-Authorization", proxyAuthorization);
- }
-
- // Always set the Proxy-Connection to Keep-Alive for the benefit of
- // HTTP/1.0 proxies like Squid.
- result.header("Proxy-Connection", "Keep-Alive");
- return result.build();
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/BitArray.java b/okhttp/src/main/java/com/squareup/okhttp/internal/BitArray.java
deleted file mode 100644
index c83f1dd..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/BitArray.java
+++ /dev/null
@@ -1,177 +0,0 @@
-/*
- * Copyright 2014 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.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-
-import static java.lang.String.format;
-
-/** A simple bitset which supports left shifting. */
-public interface BitArray {
-
- void clear();
-
- void set(int index);
-
- void toggle(int index);
-
- boolean get(int index);
-
- void shiftLeft(int count);
-
- /** Bit set that only supports settings bits 0 - 63. */
- public final class FixedCapacity implements BitArray {
- long data = 0x0000000000000000L;
-
- @Override public void clear() {
- data = 0x0000000000000000L;
- }
-
- @Override public void set(int index) {
- data |= (1L << checkInput(index));
- }
-
- @Override public void toggle(int index) {
- data ^= (1L << checkInput(index));
- }
-
- @Override public boolean get(int index) {
- return ((data >> checkInput(index)) & 1L) == 1;
- }
-
- @Override public void shiftLeft(int count) {
- data = data << checkInput(count);
- }
-
- @Override public String toString() {
- return Long.toBinaryString(data);
- }
-
- public BitArray toVariableCapacity() {
- return new VariableCapacity(this);
- }
-
- private static int checkInput(int index) {
- if (index < 0 || index > 63) {
- throw new IllegalArgumentException(format("input must be between 0 and 63: %s", index));
- }
- return index;
- }
- }
-
- /** Bit set that grows as needed. */
- public final class VariableCapacity implements BitArray {
-
- long[] data;
-
- // Start offset which allows for cheap shifting. Data is always kept on 64-bit bounds but we
- // offset the outward facing index to support shifts without having to move the underlying bits.
- private int start; // Valid values are [0..63]
-
- public VariableCapacity() {
- data = new long[1];
- }
-
- private VariableCapacity(FixedCapacity small) {
- data = new long[] {small.data, 0};
- }
-
- private void growToSize(int size) {
- long[] newData = new long[size];
- if (data != null) {
- System.arraycopy(data, 0, newData, 0, data.length);
- }
- data = newData;
- }
-
- private int offsetOf(int index) {
- index += start;
- int offset = index / 64;
- if (offset > data.length - 1) {
- growToSize(offset + 1);
- }
- return offset;
- }
-
- private int shiftOf(int index) {
- return (index + start) % 64;
- }
-
- @Override public void clear() {
- Arrays.fill(data, 0);
- }
-
- @Override public void set(int index) {
- checkInput(index);
- int offset = offsetOf(index);
- data[offset] |= 1L << shiftOf(index);
- }
-
- @Override public void toggle(int index) {
- checkInput(index);
- int offset = offsetOf(index);
- data[offset] ^= 1L << shiftOf(index);
- }
-
- @Override public boolean get(int index) {
- checkInput(index);
- int offset = offsetOf(index);
- return (data[offset] & (1L << shiftOf(index))) != 0;
- }
-
- @Override public void shiftLeft(int count) {
- start -= checkInput(count);
- if (start < 0) {
- int arrayShift = (start / -64) + 1;
- long[] newData = new long[data.length + arrayShift];
- System.arraycopy(data, 0, newData, arrayShift, data.length);
- data = newData;
- start = 64 + (start % 64);
- }
- }
-
- @Override public String toString() {
- StringBuilder builder = new StringBuilder("{");
- List<Integer> ints = toIntegerList();
- for (int i = 0, count = ints.size(); i < count; i++) {
- if (i > 0) {
- builder.append(',');
- }
- builder.append(ints.get(i));
- }
- return builder.append('}').toString();
- }
-
- List<Integer> toIntegerList() {
- List<Integer> ints = new ArrayList<Integer>();
- for (int i = 0, count = data.length * 64 - start; i < count; i++) {
- if (get(i)) {
- ints.add(i);
- }
- }
- return ints;
- }
-
- private static int checkInput(int index) {
- if (index < 0) {
- throw new IllegalArgumentException(format("input must be a positive number: %s", index));
- }
- return index;
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
index 7f4fa11..8bddc3d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/DiskLruCache.java
@@ -19,25 +19,27 @@
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
-import java.io.FileInputStream;
import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.FilterOutputStream;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
-import okio.OkBuffer;
+import okio.ForwardingSink;
import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
/**
* A cache that uses a bounded amount of space on a filesystem. Each cache
@@ -90,7 +92,7 @@
static final String MAGIC = "libcore.io.DiskLruCache";
static final String VERSION_1 = "1";
static final long ANY_SEQUENCE_NUMBER = -1;
- static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}");
+ static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,120}");
private static final String CLEAN = "CLEAN";
private static final String DIRTY = "DIRTY";
private static final String REMOVE = "REMOVE";
@@ -145,10 +147,13 @@
private final int valueCount;
private long size = 0;
private BufferedSink journalWriter;
- private final LinkedHashMap<String, Entry> lruEntries =
- new LinkedHashMap<String, Entry>(0, 0.75f, true);
+ private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);
private int redundantOpCount;
+ // Must be read and written when synchronized on 'this'.
+ private boolean initialized;
+ private boolean closed;
+
/**
* To differentiate between old and current snapshots, each entry is given
* a sequence number each time an edit is committed. A snapshot is stale if
@@ -156,14 +161,13 @@
*/
private long nextSequenceNumber = 0;
- /** This cache uses a single background thread to evict entries. */
- final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
- new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
+ /** Used to run 'cleanupRunnable' for journal rebuilds. */
+ private final Executor executor;
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
- if (journalWriter == null) {
- return; // Closed.
+ if (!initialized | closed) {
+ return; // Nothing to do
}
try {
trimToSize();
@@ -178,7 +182,7 @@
}
};
- private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
+ DiskLruCache(File directory, int appVersion, int valueCount, long maxSize, Executor executor) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
@@ -186,19 +190,57 @@
this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP);
this.valueCount = valueCount;
this.maxSize = maxSize;
+ this.executor = executor;
+ }
+
+ // Visible for testing.
+ void initialize() throws IOException {
+ assert Thread.holdsLock(this);
+
+ if (initialized) {
+ return; // Already initialized.
+ }
+
+ // If a bkp file exists, use it instead.
+ if (journalFileBackup.exists()) {
+ // If journal file also exists just delete backup file.
+ if (journalFile.exists()) {
+ journalFileBackup.delete();
+ } else {
+ renameTo(journalFileBackup, journalFile, false);
+ }
+ }
+
+ // Prefer to pick up where we left off.
+ if (journalFile.exists()) {
+ try {
+ readJournal();
+ processJournal();
+ initialized = true;
+ return;
+ } catch (IOException journalIsCorrupt) {
+ Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
+ + journalIsCorrupt.getMessage() + ", removing");
+ delete();
+ closed = false;
+ }
+ }
+
+ directory.mkdirs();
+ rebuildJournal();
+
+ initialized = true;
}
/**
- * Opens the cache in {@code directory}, creating a cache if none exists
- * there.
+ * Create a cache which will reside in {@code directory}. This cache is lazily initialized on
+ * first access and will be created if it does not exist.
*
* @param directory a writable directory
* @param valueCount the number of values per cache entry. Must be positive.
* @param maxSize the maximum number of bytes this cache should use to store
- * @throws IOException if reading or writing the cache directory fails
*/
- public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
- throws IOException {
+ public static DiskLruCache create(File directory, int appVersion, int valueCount, long maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
@@ -206,42 +248,15 @@
throw new IllegalArgumentException("valueCount <= 0");
}
- // If a bkp file exists, use it instead.
- File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
- if (backupFile.exists()) {
- File journalFile = new File(directory, JOURNAL_FILE);
- // If journal file also exists just delete backup file.
- if (journalFile.exists()) {
- backupFile.delete();
- } else {
- renameTo(backupFile, journalFile, false);
- }
- }
+ // Use a single background thread to evict entries.
+ Executor executor = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true));
- // Prefer to pick up where we left off.
- DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
- if (cache.journalFile.exists()) {
- try {
- cache.readJournal();
- cache.processJournal();
- cache.journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(cache.journalFile, true)));
- return cache;
- } catch (IOException journalIsCorrupt) {
- Platform.get().logW("DiskLruCache " + directory + " is corrupt: "
- + journalIsCorrupt.getMessage() + ", removing");
- cache.delete();
- }
- }
-
- // Create a new empty cache.
- directory.mkdirs();
- cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
- cache.rebuildJournal();
- return cache;
+ return new DiskLruCache(directory, appVersion, valueCount, maxSize, executor);
}
private void readJournal() throws IOException {
- BufferedSource source = Okio.buffer(Okio.source(new FileInputStream(journalFile)));
+ BufferedSource source = Okio.buffer(Okio.source(journalFile));
try {
String magic = source.readUtf8LineStrict();
String version = source.readUtf8LineStrict();
@@ -267,6 +282,13 @@
}
}
redundantOpCount = lineCount - lruEntries.size();
+
+ // If we ended on a truncated line, rebuild the journal before appending to it.
+ if (!source.exhausted()) {
+ rebuildJournal();
+ } else {
+ journalWriter = Okio.buffer(Okio.appendingSink(journalFile));
+ }
} finally {
Util.closeQuietly(source);
}
@@ -326,8 +348,8 @@
} else {
entry.currentEditor = null;
for (int t = 0; t < valueCount; t++) {
- deleteIfExists(entry.getCleanFile(t));
- deleteIfExists(entry.getDirtyFile(t));
+ deleteIfExists(entry.cleanFiles[t]);
+ deleteIfExists(entry.dirtyFiles[t]);
}
i.remove();
}
@@ -343,23 +365,24 @@
journalWriter.close();
}
- BufferedSink writer = Okio.buffer(Okio.sink(new FileOutputStream(journalFileTmp)));
+ BufferedSink writer = Okio.buffer(Okio.sink(journalFileTmp));
try {
- writer.writeUtf8(MAGIC);
- writer.writeUtf8("\n");
- writer.writeUtf8(VERSION_1);
- writer.writeUtf8("\n");
- writer.writeUtf8(Integer.toString(appVersion));
- writer.writeUtf8("\n");
- writer.writeUtf8(Integer.toString(valueCount));
- writer.writeUtf8("\n");
- writer.writeUtf8("\n");
+ writer.writeUtf8(MAGIC).writeByte('\n');
+ writer.writeUtf8(VERSION_1).writeByte('\n');
+ writer.writeUtf8(Integer.toString(appVersion)).writeByte('\n');
+ writer.writeUtf8(Integer.toString(valueCount)).writeByte('\n');
+ writer.writeByte('\n');
for (Entry entry : lruEntries.values()) {
if (entry.currentEditor != null) {
- writer.writeUtf8(DIRTY + ' ' + entry.key + '\n');
+ writer.writeUtf8(DIRTY).writeByte(' ');
+ writer.writeUtf8(entry.key);
+ writer.writeByte('\n');
} else {
- writer.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ writer.writeUtf8(CLEAN).writeByte(' ');
+ writer.writeUtf8(entry.key);
+ entry.writeLengths(writer);
+ writer.writeByte('\n');
}
}
} finally {
@@ -372,7 +395,7 @@
renameTo(journalFileTmp, journalFile, false);
journalFileBackup.delete();
- journalWriter = Okio.buffer(Okio.sink(new FileOutputStream(journalFile, true)));
+ journalWriter = Okio.buffer(Okio.appendingSink(journalFile));
}
private static void deleteIfExists(File file) throws IOException {
@@ -397,44 +420,23 @@
* the head of the LRU queue.
*/
public synchronized Snapshot get(String key) throws IOException {
+ initialize();
+
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
- if (entry == null) {
- return null;
- }
+ if (entry == null || !entry.readable) return null;
- if (!entry.readable) {
- return null;
- }
-
- // Open all streams eagerly to guarantee that we see a single published
- // snapshot. If we opened streams lazily then the streams could come
- // from different edits.
- InputStream[] ins = new InputStream[valueCount];
- try {
- for (int i = 0; i < valueCount; i++) {
- ins[i] = new FileInputStream(entry.getCleanFile(i));
- }
- } catch (FileNotFoundException e) {
- // A file must have been deleted manually!
- for (int i = 0; i < valueCount; i++) {
- if (ins[i] != null) {
- Util.closeQuietly(ins[i]);
- } else {
- break;
- }
- }
- return null;
- }
+ Snapshot snapshot = entry.snapshot();
+ if (snapshot == null) return null;
redundantOpCount++;
- journalWriter.writeUtf8(READ + ' ' + key + '\n');
+ journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');
if (journalRebuildRequired()) {
- executorService.execute(cleanupRunnable);
+ executor.execute(cleanupRunnable);
}
- return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
+ return snapshot;
}
/**
@@ -446,6 +448,8 @@
}
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
+ initialize();
+
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
@@ -464,7 +468,7 @@
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
- journalWriter.writeUtf8(DIRTY + ' ' + key + '\n');
+ journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
journalWriter.flush();
return editor;
}
@@ -488,7 +492,9 @@
*/
public synchronized void setMaxSize(long maxSize) {
this.maxSize = maxSize;
- executorService.execute(cleanupRunnable);
+ if (initialized) {
+ executor.execute(cleanupRunnable);
+ }
}
/**
@@ -496,7 +502,8 @@
* this cache. This may be greater than the max size if a background
* deletion is pending.
*/
- public synchronized long size() {
+ public synchronized long size() throws IOException {
+ initialize();
return size;
}
@@ -513,7 +520,7 @@
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
- if (!entry.getDirtyFile(i).exists()) {
+ if (!entry.dirtyFiles[i].exists()) {
editor.abort();
return;
}
@@ -521,10 +528,10 @@
}
for (int i = 0; i < valueCount; i++) {
- File dirty = entry.getDirtyFile(i);
+ File dirty = entry.dirtyFiles[i];
if (success) {
if (dirty.exists()) {
- File clean = entry.getCleanFile(i);
+ File clean = entry.cleanFiles[i];
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
@@ -540,18 +547,23 @@
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
- journalWriter.writeUtf8(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
+ journalWriter.writeUtf8(CLEAN).writeByte(' ');
+ journalWriter.writeUtf8(entry.key);
+ entry.writeLengths(journalWriter);
+ journalWriter.writeByte('\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
- journalWriter.writeUtf8(REMOVE + ' ' + entry.key + '\n');
+ journalWriter.writeUtf8(REMOVE).writeByte(' ');
+ journalWriter.writeUtf8(entry.key);
+ journalWriter.writeByte('\n');
}
journalWriter.flush();
if (size > maxSize || journalRebuildRequired()) {
- executorService.execute(cleanupRunnable);
+ executor.execute(cleanupRunnable);
}
}
@@ -566,50 +578,60 @@
}
/**
- * Drops the entry for {@code key} if it exists and can be removed. Entries
- * actively being edited cannot be removed.
+ * Drops the entry for {@code key} if it exists and can be removed. If the
+ * entry for {@code key} is currently being edited, that edit will complete
+ * normally but its value will not be stored.
*
* @return true if an entry was removed.
*/
public synchronized boolean remove(String key) throws IOException {
+ initialize();
+
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
- if (entry == null || entry.currentEditor != null) {
- return false;
+ if (entry == null) return false;
+ return removeEntry(entry);
+ }
+
+ private boolean removeEntry(Entry entry) throws IOException {
+ if (entry.currentEditor != null) {
+ entry.currentEditor.hasErrors = true; // Prevent the edit from completing normally.
}
for (int i = 0; i < valueCount; i++) {
- File file = entry.getCleanFile(i);
+ File file = entry.cleanFiles[i];
deleteIfExists(file);
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
redundantOpCount++;
- journalWriter.writeUtf8(REMOVE + ' ' + key + '\n');
- lruEntries.remove(key);
+ journalWriter.writeUtf8(REMOVE).writeByte(' ').writeUtf8(entry.key).writeByte('\n');
+ lruEntries.remove(entry.key);
if (journalRebuildRequired()) {
- executorService.execute(cleanupRunnable);
+ executor.execute(cleanupRunnable);
}
return true;
}
/** Returns true if this cache has been closed. */
- public boolean isClosed() {
- return journalWriter == null;
+ public synchronized boolean isClosed() {
+ return closed;
}
- private void checkNotClosed() {
- if (journalWriter == null) {
+ private synchronized void checkNotClosed() {
+ if (isClosed()) {
throw new IllegalStateException("cache is closed");
}
}
/** Force buffered operations to the filesystem. */
public synchronized void flush() throws IOException {
+ if (!initialized) return;
+
checkNotClosed();
trimToSize();
journalWriter.flush();
@@ -617,12 +639,12 @@
/** Closes this cache. Stored values will remain on the filesystem. */
public synchronized void close() throws IOException {
- if (journalWriter == null) {
- return; // Already closed.
+ if (!initialized || closed) {
+ closed = true;
+ return;
}
// Copying for safe iteration.
- for (Object next : lruEntries.values().toArray()) {
- Entry entry = (Entry) next;
+ for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) {
if (entry.currentEditor != null) {
entry.currentEditor.abort();
}
@@ -630,12 +652,13 @@
trimToSize();
journalWriter.close();
journalWriter = null;
+ closed = true;
}
private void trimToSize() throws IOException {
while (size > maxSize) {
- Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
- remove(toEvict.getKey());
+ Entry toEvict = lruEntries.values().iterator().next();
+ removeEntry(toEvict);
}
}
@@ -649,32 +672,111 @@
Util.deleteContents(directory);
}
- private void validateKey(String key) {
- Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
- if (!matcher.matches()) {
- throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\"");
+ /**
+ * Deletes all stored values from the cache. In-flight edits will complete
+ * normally but their values will not be stored.
+ */
+ public synchronized void evictAll() throws IOException {
+ initialize();
+ // Copying for safe iteration.
+ for (Entry entry : lruEntries.values().toArray(new Entry[lruEntries.size()])) {
+ removeEntry(entry);
}
}
- private static String inputStreamToString(InputStream in) throws IOException {
- OkBuffer buffer = Util.readFully(Okio.source(in));
- return buffer.readUtf8(buffer.size());
+ private void validateKey(String key) {
+ Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+ if (!matcher.matches()) {
+ throw new IllegalArgumentException(
+ "keys must match regex [a-z0-9_-]{1,120}: \"" + key + "\"");
+ }
+ }
+
+ /**
+ * Returns an iterator over the cache's current entries. This iterator doesn't throw {@code
+ * ConcurrentModificationException}, but if new entries are added while iterating, those new
+ * entries will not be returned by the iterator. If existing entries are removed during iteration,
+ * they will be absent (unless they were already returned).
+ *
+ * <p>If there are I/O problems during iteration, this iterator fails silently. For example, if
+ * the hosting filesystem becomes unreachable, the iterator will omit elements rather than
+ * throwing exceptions.
+ *
+ * <p><strong>The caller must {@link Snapshot#close close}</strong> each snapshot returned by
+ * {@link Iterator#next}. Failing to do so leaks open files!
+ *
+ * <p>The returned iterator supports {@link Iterator#remove}.
+ */
+ public synchronized Iterator<Snapshot> snapshots() throws IOException {
+ initialize();
+ return new Iterator<Snapshot>() {
+ /** Iterate a copy of the entries to defend against concurrent modification errors. */
+ final Iterator<Entry> delegate = new ArrayList<>(lruEntries.values()).iterator();
+
+ /** The snapshot to return from {@link #next}. Null if we haven't computed that yet. */
+ Snapshot nextSnapshot;
+
+ /** The snapshot to remove with {@link #remove}. Null if removal is illegal. */
+ Snapshot removeSnapshot;
+
+ @Override public boolean hasNext() {
+ if (nextSnapshot != null) return true;
+
+ synchronized (DiskLruCache.this) {
+ // If the cache is closed, truncate the iterator.
+ if (closed) return false;
+
+ while (delegate.hasNext()) {
+ Entry entry = delegate.next();
+ Snapshot snapshot = entry.snapshot();
+ if (snapshot == null) continue; // Evicted since we copied the entries.
+ nextSnapshot = snapshot;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override public Snapshot next() {
+ if (!hasNext()) throw new NoSuchElementException();
+ removeSnapshot = nextSnapshot;
+ nextSnapshot = null;
+ return removeSnapshot;
+ }
+
+ @Override public void remove() {
+ if (removeSnapshot == null) throw new IllegalStateException("remove() before next()");
+ try {
+ DiskLruCache.this.remove(removeSnapshot.key);
+ } catch (IOException ignored) {
+ // Nothing useful to do here. We failed to remove from the cache. Most likely that's
+ // because we couldn't update the journal, but the cached entry will still be gone.
+ } finally {
+ removeSnapshot = null;
+ }
+ }
+ };
}
/** A snapshot of the values for an entry. */
public final class Snapshot implements Closeable {
private final String key;
private final long sequenceNumber;
- private final InputStream[] ins;
+ private final Source[] sources;
private final long[] lengths;
- private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
+ private Snapshot(String key, long sequenceNumber, Source[] sources, long[] lengths) {
this.key = key;
this.sequenceNumber = sequenceNumber;
- this.ins = ins;
+ this.sources = sources;
this.lengths = lengths;
}
+ public String key() {
+ return key;
+ }
+
/**
* Returns an editor for this snapshot's entry, or null if either the
* entry has changed since this snapshot was created or if another edit
@@ -685,13 +787,8 @@
}
/** Returns the unbuffered stream with the value for {@code index}. */
- public InputStream getInputStream(int index) {
- return ins[index];
- }
-
- /** Returns the string value for {@code index}. */
- public String getString(int index) throws IOException {
- return inputStreamToString(getInputStream(index));
+ public Source getSource(int index) {
+ return sources[index];
}
/** Returns the byte length of the value for {@code index}. */
@@ -700,16 +797,25 @@
}
public void close() {
- for (InputStream in : ins) {
+ for (Source in : sources) {
Util.closeQuietly(in);
}
}
}
- private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() {
- @Override
- public void write(int b) throws IOException {
- // Eat all writes silently. Nom nom.
+ private static final Sink NULL_SINK = new Sink() {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ source.skip(byteCount);
+ }
+
+ @Override public void flush() throws IOException {
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
}
};
@@ -729,7 +835,7 @@
* Returns an unbuffered input stream to read the last committed value,
* or null if no value has been committed.
*/
- public InputStream newInputStream(int index) throws IOException {
+ public Source newSource(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
@@ -738,7 +844,7 @@
return null;
}
try {
- return new FileInputStream(entry.getCleanFile(index));
+ return Okio.source(entry.cleanFiles[index]);
} catch (FileNotFoundException e) {
return null;
}
@@ -746,22 +852,13 @@
}
/**
- * Returns the last committed value as a string, or null if no value
- * has been committed.
- */
- public String getString(int index) throws IOException {
- InputStream in = newInputStream(index);
- return in != null ? inputStreamToString(in) : null;
- }
-
- /**
* Returns a new unbuffered output stream to write the value at
* {@code index}. If the underlying output stream encounters errors
* when writing to the filesystem, this edit will be aborted when
* {@link #commit} is called. The returned output stream does not throw
* IOExceptions.
*/
- public OutputStream newOutputStream(int index) throws IOException {
+ public Sink newSink(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
@@ -769,43 +866,38 @@
if (!entry.readable) {
written[index] = true;
}
- File dirtyFile = entry.getDirtyFile(index);
- FileOutputStream outputStream;
+ File dirtyFile = entry.dirtyFiles[index];
+ Sink sink;
try {
- outputStream = new FileOutputStream(dirtyFile);
+ sink = Okio.sink(dirtyFile);
} catch (FileNotFoundException e) {
// Attempt to recreate the cache directory.
directory.mkdirs();
try {
- outputStream = new FileOutputStream(dirtyFile);
+ sink = Okio.sink(dirtyFile);
} catch (FileNotFoundException e2) {
// We are unable to recover. Silently eat the writes.
- return NULL_OUTPUT_STREAM;
+ return NULL_SINK;
}
}
- return new FaultHidingOutputStream(outputStream);
+ return new FaultHidingSink(sink);
}
}
- /** Sets the value at {@code index} to {@code value}. */
- public void set(int index, String value) throws IOException {
- BufferedSink writer = Okio.buffer(Okio.sink(newOutputStream(index)));
- writer.writeUtf8(value);
- writer.close();
- }
-
/**
* Commits this edit so it is visible to readers. This releases the
* edit lock so another edit may be started on the same key.
*/
public void commit() throws IOException {
- if (hasErrors) {
- completeEdit(this, false);
- remove(entry.key); // The previous entry is stale.
- } else {
- completeEdit(this, true);
+ synchronized (DiskLruCache.this) {
+ if (hasErrors) {
+ completeEdit(this, false);
+ removeEntry(entry); // The previous entry is stale.
+ } else {
+ completeEdit(this, true);
+ }
+ committed = true;
}
- committed = true;
}
/**
@@ -813,52 +905,54 @@
* started on the same key.
*/
public void abort() throws IOException {
- completeEdit(this, false);
+ synchronized (DiskLruCache.this) {
+ completeEdit(this, false);
+ }
}
public void abortUnlessCommitted() {
- if (!committed) {
- try {
- abort();
- } catch (IOException ignored) {
+ synchronized (DiskLruCache.this) {
+ if (!committed) {
+ try {
+ completeEdit(this, false);
+ } catch (IOException ignored) {
+ }
}
}
}
- private class FaultHidingOutputStream extends FilterOutputStream {
- private FaultHidingOutputStream(OutputStream out) {
- super(out);
+ private class FaultHidingSink extends ForwardingSink {
+ public FaultHidingSink(Sink delegate) {
+ super(delegate);
}
- @Override public void write(int oneByte) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
try {
- out.write(oneByte);
+ super.write(source, byteCount);
} catch (IOException e) {
- hasErrors = true;
+ synchronized (DiskLruCache.this) {
+ hasErrors = true;
+ }
}
}
- @Override public void write(byte[] buffer, int offset, int length) {
+ @Override public void flush() throws IOException {
try {
- out.write(buffer, offset, length);
+ super.flush();
} catch (IOException e) {
- hasErrors = true;
+ synchronized (DiskLruCache.this) {
+ hasErrors = true;
+ }
}
}
- @Override public void close() {
+ @Override public void close() throws IOException {
try {
- out.close();
+ super.close();
} catch (IOException e) {
- hasErrors = true;
- }
- }
-
- @Override public void flush() {
- try {
- out.flush();
- } catch (IOException e) {
- hasErrors = true;
+ synchronized (DiskLruCache.this) {
+ hasErrors = true;
+ }
}
}
}
@@ -869,6 +963,8 @@
/** Lengths of this entry's files. */
private final long[] lengths;
+ private final File[] cleanFiles;
+ private final File[] dirtyFiles;
/** True if this entry has ever been published. */
private boolean readable;
@@ -881,15 +977,21 @@
private Entry(String key) {
this.key = key;
- this.lengths = new long[valueCount];
- }
- public String getLengths() throws IOException {
- StringBuilder result = new StringBuilder();
- for (long size : lengths) {
- result.append(' ').append(size);
+ lengths = new long[valueCount];
+ cleanFiles = new File[valueCount];
+ dirtyFiles = new File[valueCount];
+
+ // The names are repetitive so re-use the same builder to avoid allocations.
+ StringBuilder fileBuilder = new StringBuilder(key).append('.');
+ int truncateTo = fileBuilder.length();
+ for (int i = 0; i < valueCount; i++) {
+ fileBuilder.append(i);
+ cleanFiles[i] = new File(directory, fileBuilder.toString());
+ fileBuilder.append(".tmp");
+ dirtyFiles[i] = new File(directory, fileBuilder.toString());
+ fileBuilder.setLength(truncateTo);
}
- return result.toString();
}
/** Set lengths using decimal numbers like "10123". */
@@ -907,16 +1009,43 @@
}
}
+ /** Append space-prefixed lengths to {@code writer}. */
+ void writeLengths(BufferedSink writer) throws IOException {
+ for (long length : lengths) {
+ writer.writeByte(' ').writeUtf8(Long.toString(length));
+ }
+ }
+
private IOException invalidLengths(String[] strings) throws IOException {
- throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
+ throw new IOException("unexpected journal line: " + Arrays.toString(strings));
}
- public File getCleanFile(int i) {
- return new File(directory, key + "." + i);
- }
+ /**
+ * Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a
+ * single published snapshot. If we opened streams lazily then the streams could come from
+ * different edits.
+ */
+ Snapshot snapshot() {
+ if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();
- public File getDirtyFile(int i) {
- return new File(directory, key + "." + i + ".tmp");
+ Source[] sources = new Source[valueCount];
+ long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.
+ try {
+ for (int i = 0; i < valueCount; i++) {
+ sources[i] = Okio.source(cleanFiles[i]);
+ }
+ return new Snapshot(key, sequenceNumber, sources, lengths);
+ } catch (FileNotFoundException e) {
+ // A file must have been deleted manually!
+ for (int i = 0; i < valueCount; i++) {
+ if (sources[i] != null) {
+ Util.closeQuietly(sources[i]);
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
}
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
new file mode 100644
index 0000000..d806b48
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 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 com.squareup.okhttp.Call;
+import com.squareup.okhttp.Callback;
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Headers;
+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.Transport;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+/**
+ * Escalate internal APIs in {@code com.squareup.okhttp} so they can be used
+ * from OkHttp's implementation packages. The only implementation of this
+ * interface is in {@link com.squareup.okhttp.OkHttpClient}.
+ */
+public abstract class Internal {
+ public static final Logger logger = Logger.getLogger(OkHttpClient.class.getName());
+ 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 setCache(OkHttpClient client, InternalCache internalCache);
+
+ public abstract InternalCache internalCache(OkHttpClient client);
+
+ public abstract void recycle(ConnectionPool pool, Connection connection);
+
+ public abstract RouteDatabase routeDatabase(OkHttpClient client);
+
+ public abstract Network network(OkHttpClient client);
+
+ public abstract void setNetwork(OkHttpClient client, Network network);
+
+ public abstract void connectAndSetOwner(OkHttpClient client, Connection connection,
+ HttpEngine owner, Request request) throws IOException;
+
+ // 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 void connectionSetOwner(Connection connection, Object owner);
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java b/okhttp/src/main/java/com/squareup/okhttp/internal/InternalCache.java
similarity index 62%
rename from okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/InternalCache.java
index 05460f5..4925358 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkResponseCache.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/InternalCache.java
@@ -13,27 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp;
+package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.http.CacheRequest;
+import com.squareup.okhttp.internal.http.CacheStrategy;
import java.io.IOException;
-import java.net.CacheRequest;
/**
- * An extended response cache API. Unlike {@link java.net.ResponseCache}, this
- * interface supports conditional caching and statistics.
+ * OkHttp's internal cache interface. Applications shouldn't implement this:
+ * instead use {@link com.squareup.okhttp.Cache}.
*/
-public interface OkResponseCache {
+public interface InternalCache {
Response get(Request request) throws IOException;
CacheRequest put(Response response) throws IOException;
/**
- * Remove any cache entries for the supplied {@code uri}. Returns true if the
- * supplied {@code requestMethod} potentially invalidates an entry in the
- * cache.
+ * Remove any cache entries for the supplied {@code request}. This is invoked
+ * when the client invalidates the cache, such as when making POST requests.
*/
- // TODO: this shouldn't return a boolean.
- boolean maybeRemove(Request request) throws IOException;
+ void remove(Request request) throws IOException;
/**
* Handles a conditional request hit by updating the stored cache response
@@ -46,6 +47,6 @@
/** Track an conditional GET that was satisfied by this cache. */
void trackConditionalCacheHit();
- /** Track an HTTP response being satisfied by {@code source}. */
- void trackResponse(ResponseSource source);
+ /** Track an HTTP response being satisfied with {@code cacheStrategy}. */
+ void trackResponse(CacheStrategy cacheStrategy);
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java b/okhttp/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
index 992b2ae..7a02ecf 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/NamedRunnable.java
@@ -20,7 +20,7 @@
* Runnable implementation which always sets its thread name.
*/
public abstract class NamedRunnable implements Runnable {
- private final String name;
+ protected final String name;
public NamedRunnable(String format, Object... args) {
this.name = String.format(format, args);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HostResolver.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
similarity index 65%
rename from okhttp/src/main/java/com/squareup/okhttp/HostResolver.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
index c7a1edb..a007065 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HostResolver.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
@@ -13,22 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp;
+package com.squareup.okhttp.internal;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
- * Domain name service. Prefer this over {@link InetAddress#getAllByName} to
- * make code more testable.
+ * Services specific to the host device's network interface. Prefer this over {@link
+ * InetAddress#getAllByName} to make code more testable.
*/
-public interface HostResolver {
- HostResolver DEFAULT = new HostResolver() {
- @Override public InetAddress[] getAllByName(String host) throws UnknownHostException {
+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[] getAllByName(String host) throws UnknownHostException;
+ InetAddress[] resolveInetAddresses(String host) throws UnknownHostException;
}
diff --git a/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java b/okhttp/src/main/java/com/squareup/okhttp/internal/OptionalMethod.java
similarity index 93%
rename from android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/OptionalMethod.java
index 81aef8e..21b31cc 100644
--- a/android/main/java/com/squareup/okhttp/internal/OptionalMethod.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/OptionalMethod.java
@@ -90,7 +90,9 @@
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
- throw new AssertionError("Unexpected exception", targetException);
+ AssertionError error = new AssertionError("Unexpected exception");
+ error.initCause(targetException);
+ throw error;
}
}
@@ -110,7 +112,9 @@
return m.invoke(target, args);
} catch (IllegalAccessException e) {
// Method should be public: we checked.
- throw new AssertionError("Unexpectedly could not call: " + m, e);
+ AssertionError error = new AssertionError("Unexpectedly could not call: " + m);
+ error.initCause(e);
+ throw error;
}
}
@@ -129,7 +133,9 @@
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
- throw new AssertionError("Unexpected exception", targetException);
+ AssertionError error = new AssertionError("Unexpected exception");
+ error.initCause(targetException);
+ throw error;
}
}
@@ -167,3 +173,4 @@
return method;
}
}
+
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 28df601..14b5fb1 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -17,10 +17,7 @@
package com.squareup.okhttp.internal;
import com.squareup.okhttp.Protocol;
-
import java.io.IOException;
-import java.io.OutputStream;
-import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -34,43 +31,25 @@
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
-import java.util.logging.Logger;
-import java.util.zip.Deflater;
-import java.util.zip.DeflaterOutputStream;
import javax.net.ssl.SSLSocket;
-import okio.ByteString;
+import okio.Buffer;
+
+import static com.squareup.okhttp.internal.Internal.logger;
/**
* Access to Platform-specific features necessary for SPDY and advanced TLS.
+ * This includes Server Name Indication (SNI) and session tickets.
*
- * <h3>ALPN and NPN</h3>
- * This class uses TLS extensions ALPN and NPN to negotiate the upgrade from
- * HTTP/1.1 (the default protocol to use with TLS on port 443) to either SPDY
- * or HTTP/2.
+ * <h3>ALPN</h3>
+ * This class uses TLS extension ALPN to negotiate the upgrade from HTTP/1.1
+ * (the default protocol to use with TLS on port 443) to either SPDY or HTTP/2.
*
- * <p>NPN (Next Protocol Negotiation) was developed for SPDY. It is widely
- * available and we support it on both Android (4.1+) and OpenJDK 7 (via the
- * Jetty NPN-boot library). NPN is not yet available on Java 8.
- *
- * <p>ALPN (Application Layer Protocol Negotiation) is the successor to NPN. It
- * has some technical advantages over NPN. ALPN first arrived in Android 4.4,
- * but that release suffers a <a href="http://goo.gl/y5izPP">concurrency bug</a>
- * so we don't use it. ALPN will be supported in the future.
- *
- * <p>On platforms that support both extensions, OkHttp will use both,
- * preferring ALPN's result. Future versions of OkHttp will drop support for
- * NPN.
- *
- * <h3>Deflater Sync Flush</h3>
- * SPDY header compression requires a recent version of {@code
- * DeflaterOutputStream} that is public API in Java 7 and callable via
- * reflection in Android 4.1+.
+ * <p>ALPN (Application Layer Protocol Negotiation) first arrived in Android 4.4,
+ * ALPN is supported on OpenJDK 7 and 8 (via the Jetty ALPN-boot library).
*/
public class Platform {
private static final Platform PLATFORM = findPlatform();
- private Constructor<DeflaterOutputStream> deflaterConstructor;
-
public static Platform get() {
return PLATFORM;
}
@@ -94,19 +73,26 @@
return url.toURI(); // this isn't as good as the built-in toUriLenient
}
- public void configureSecureSocket(SSLSocket socket, String uriHost, boolean isFallback) {
- }
-
- /** Returns the negotiated protocol, or null if no protocol was negotiated. */
- public ByteString getNpnSelectedProtocol(SSLSocket socket) {
- return null;
+ /**
+ * Configure TLS extensions on {@code sslSocket} for {@code route}.
+ *
+ * @param hostname non-null for client-side handshakes; null for
+ * server-side handshakes.
+ */
+ public void configureTlsExtensions(SSLSocket sslSocket, String hostname,
+ List<Protocol> protocols) {
}
/**
- * Sets client-supported protocols on a socket to send to a server. The
- * protocols are only sent if the socket implementation supports NPN.
+ * Called after the TLS handshake to release resources allocated by {@link
+ * #configureTlsExtensions}.
*/
- public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
+ public void afterHandshake(SSLSocket sslSocket) {
+ }
+
+ /** Returns the negotiated protocol, or null if no protocol was negotiated. */
+ public String getSelectedProtocol(SSLSocket socket) {
+ return null;
}
public void connectSocket(Socket socket, InetSocketAddress address,
@@ -114,82 +100,46 @@
socket.connect(address, connectTimeout);
}
- /**
- * Returns a deflater output stream that supports SYNC_FLUSH for SPDY name
- * value blocks. This throws an {@link UnsupportedOperationException} on
- * Java 6 and earlier where there is no built-in API to do SYNC_FLUSH.
- */
- public OutputStream newDeflaterOutputStream(OutputStream out, Deflater deflater,
- boolean syncFlush) {
- try {
- Constructor<DeflaterOutputStream> constructor = deflaterConstructor;
- if (constructor == null) {
- constructor = deflaterConstructor = DeflaterOutputStream.class.getConstructor(
- OutputStream.class, Deflater.class, boolean.class);
- }
- return constructor.newInstance(out, deflater, syncFlush);
- } catch (NoSuchMethodException e) {
- throw new UnsupportedOperationException("Cannot SPDY; no SYNC_FLUSH available");
- } catch (InvocationTargetException e) {
- throw e.getCause() instanceof RuntimeException ? (RuntimeException) e.getCause()
- : new RuntimeException(e.getCause());
- } catch (InstantiationException e) {
- throw new RuntimeException(e);
- } catch (IllegalAccessException e) {
- throw new AssertionError();
- }
- }
-
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
// Attempt to find Android 2.3+ APIs.
- Class<?> openSslSocketClass;
- Method setUseSessionTickets;
- Method setHostname;
try {
try {
- openSslSocketClass = Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
+ Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
} catch (ClassNotFoundException ignored) {
// Older platform before being unbundled.
- openSslSocketClass = Class.forName(
- "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+ Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
}
- setUseSessionTickets = openSslSocketClass.getMethod("setUseSessionTickets", boolean.class);
- setHostname = openSslSocketClass.getMethod("setHostname", String.class);
-
- // Attempt to find Android 4.1+ APIs.
- Method setNpnProtocols = null;
- Method getNpnSelectedProtocol = null;
+ // Attempt to find Android 4.0+ APIs.
+ Method trafficStatsTagSocket = null;
+ Method trafficStatsUntagSocket = null;
try {
- setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
- getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
+ Class<?> trafficStats = Class.forName("android.net.TrafficStats");
+ trafficStatsTagSocket = trafficStats.getMethod("tagSocket", Socket.class);
+ trafficStatsUntagSocket = trafficStats.getMethod("untagSocket", Socket.class);
+ } catch (ClassNotFoundException ignored) {
} catch (NoSuchMethodException ignored) {
}
- return new Android(openSslSocketClass, setUseSessionTickets, setHostname, setNpnProtocols,
- getNpnSelectedProtocol);
+ return new Android(trafficStatsTagSocket, trafficStatsUntagSocket);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
- } catch (NoSuchMethodException ignored) {
- // This isn't Android 2.3 or better.
}
- // Attempt to find the Jetty's NPN extension for OpenJDK.
- try {
- String npnClassName = "org.eclipse.jetty.npn.NextProtoNego";
- Class<?> nextProtoNegoClass = Class.forName(npnClassName);
- Class<?> providerClass = Class.forName(npnClassName + "$Provider");
- Class<?> clientProviderClass = Class.forName(npnClassName + "$ClientProvider");
- Class<?> serverProviderClass = Class.forName(npnClassName + "$ServerProvider");
- Method putMethod = nextProtoNegoClass.getMethod("put", SSLSocket.class, providerClass);
- Method getMethod = nextProtoNegoClass.getMethod("get", SSLSocket.class);
- return new JdkWithJettyNpnPlatform(
- putMethod, getMethod, clientProviderClass, serverProviderClass);
+ try { // to find the Jetty's ALPN extension for OpenJDK.
+ 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 ignored) {
- // NPN isn't on the classpath.
- } catch (NoSuchMethodException ignored) {
- // The NPN version isn't what we expect.
+ } catch (NoSuchMethodException ignored) { // The ALPN version isn't what we expect.
}
return new Platform();
@@ -197,25 +147,30 @@
/**
* Android 2.3 or better. Version 2.3 supports TLS session tickets and server
- * name indication (SNI). Versions 4.1 supports NPN.
+ * name indication (SNI). Versions 4.4 supports ALPN.
*/
private static class Android extends Platform {
- // Non-null.
- protected final Class<?> openSslSocketClass;
- private final Method setUseSessionTickets;
- private final Method setHostname;
- // Non-null on Android 4.1+.
- private final Method setNpnProtocols;
- private final Method getNpnSelectedProtocol;
+ // setUseSessionTickets(boolean)
+ private static final OptionalMethod<Socket> SET_USE_SESSION_TICKETS =
+ new OptionalMethod<Socket>(null, "setUseSessionTickets", Boolean.TYPE);
+ // setHostname(String)
+ private static final OptionalMethod<Socket> SET_HOSTNAME =
+ new OptionalMethod<Socket>(null, "setHostname", String.class);
+ // byte[] getAlpnSelectedProtocol()
+ private static final OptionalMethod<Socket> GET_ALPN_SELECTED_PROTOCOL =
+ new OptionalMethod<Socket>(byte[].class, "getAlpnSelectedProtocol");
+ // setAlpnSelectedProtocol(byte[])
+ private static final OptionalMethod<Socket> SET_ALPN_PROTOCOLS =
+ new OptionalMethod<Socket>(null, "setAlpnProtocols", byte[].class);
- private Android(Class<?> openSslSocketClass, Method setUseSessionTickets, Method setHostname,
- Method setNpnProtocols, Method getNpnSelectedProtocol) {
- this.openSslSocketClass = openSslSocketClass;
- this.setUseSessionTickets = setUseSessionTickets;
- this.setHostname = setHostname;
- this.setNpnProtocols = setNpnProtocols;
- this.getNpnSelectedProtocol = getNpnSelectedProtocol;
+ // Non-null on Android 4.0+.
+ private final Method trafficStatsTagSocket;
+ private final Method trafficStatsUntagSocket;
+
+ private Android(Method trafficStatsTagSocket, Method trafficStatsUntagSocket) {
+ this.trafficStatsTagSocket = trafficStatsTagSocket;
+ this.trafficStatsUntagSocket = trafficStatsUntagSocket;
}
@Override public void connectSocket(Socket socket, InetSocketAddress address,
@@ -231,73 +186,94 @@
}
}
- @Override public void configureSecureSocket(SSLSocket socket, String uriHost,
- boolean isFallback) {
+ @Override public void configureTlsExtensions(
+ SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
+ // Enable SNI and session tickets.
+ if (hostname != null) {
+ SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(sslSocket, true);
+ SET_HOSTNAME.invokeOptionalWithoutCheckedException(sslSocket, hostname);
+ }
- super.configureSecureSocket(socket, uriHost, isFallback);
- if (!openSslSocketClass.isInstance(socket)) return;
+ // Enable ALPN.
+ boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(sslSocket);
+ if (!alpnSupported) {
+ return;
+ }
+
+ Object[] parameters = { concatLengthPrefixed(protocols) };
+ SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
+ }
+
+ @Override public String getSelectedProtocol(SSLSocket socket) {
+ boolean alpnSupported = GET_ALPN_SELECTED_PROTOCOL.isSupported(socket);
+ if (!alpnSupported) {
+ return null;
+ }
+
+ byte[] alpnResult =
+ (byte[]) GET_ALPN_SELECTED_PROTOCOL.invokeWithoutCheckedException(socket);
+ if (alpnResult != null) {
+ return new String(alpnResult, Util.UTF_8);
+ }
+ return null;
+ }
+
+ @Override public void tagSocket(Socket socket) throws SocketException {
+ if (trafficStatsTagSocket == null) return;
+
try {
- setUseSessionTickets.invoke(socket, true);
- setHostname.invoke(socket, uriHost);
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e);
+ trafficStatsTagSocket.invoke(null, socket);
} catch (IllegalAccessException e) {
- throw new AssertionError(e);
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
}
}
- @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
- if (setNpnProtocols == null) return;
- if (!openSslSocketClass.isInstance(socket)) return;
- try {
- Object[] parameters = { concatLengthPrefixed(npnProtocols) };
- setNpnProtocols.invoke(socket, parameters);
- } catch (IllegalAccessException e) {
- throw new AssertionError(e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e);
- }
- }
+ @Override public void untagSocket(Socket socket) throws SocketException {
+ if (trafficStatsUntagSocket == null) return;
- @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) {
- if (getNpnSelectedProtocol == null) return null;
- if (!openSslSocketClass.isInstance(socket)) return null;
try {
- byte[] npnResult = (byte[]) getNpnSelectedProtocol.invoke(socket);
- if (npnResult == null) return null;
- return ByteString.of(npnResult);
- } catch (InvocationTargetException e) {
- throw new RuntimeException(e);
+ trafficStatsUntagSocket.invoke(null, socket);
} catch (IllegalAccessException e) {
- throw new AssertionError(e);
+ throw new RuntimeException(e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException(e.getCause());
}
}
}
- /** OpenJDK 7 plus {@code org.mortbay.jetty.npn/npn-boot} on the boot class path. */
- private static class JdkWithJettyNpnPlatform extends Platform {
- private final Method getMethod;
+ /**
+ * OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path.
+ */
+ private static class JdkWithJettyBootPlatform extends Platform {
private final Method putMethod;
+ private final Method getMethod;
+ private final Method removeMethod;
private final Class<?> clientProviderClass;
private final Class<?> serverProviderClass;
- public JdkWithJettyNpnPlatform(Method putMethod, Method getMethod, Class<?> clientProviderClass,
- Class<?> serverProviderClass) {
+ public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod,
+ Class<?> clientProviderClass, Class<?> serverProviderClass) {
this.putMethod = putMethod;
this.getMethod = getMethod;
+ this.removeMethod = removeMethod;
this.clientProviderClass = clientProviderClass;
this.serverProviderClass = serverProviderClass;
}
- @Override public void setNpnProtocols(SSLSocket socket, List<Protocol> npnProtocols) {
+ @Override public void configureTlsExtensions(
+ SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
+ List<String> names = new ArrayList<>(protocols.size());
+ for (int i = 0, size = protocols.size(); i < size; i++) {
+ Protocol protocol = protocols.get(i);
+ if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
+ names.add(protocol.toString());
+ }
try {
- List<String> names = new ArrayList<String>(npnProtocols.size());
- for (int i = 0, size = npnProtocols.size(); i < size; i++) {
- names.add(npnProtocols.get(i).name.utf8());
- }
Object provider = Proxy.newProxyInstance(Platform.class.getClassLoader(),
- new Class[] { clientProviderClass, serverProviderClass }, new JettyNpnProvider(names));
- putMethod.invoke(null, socket, provider);
+ new Class[] { clientProviderClass, serverProviderClass }, new JettyNegoProvider(names));
+ putMethod.invoke(null, sslSocket, provider);
} catch (InvocationTargetException e) {
throw new AssertionError(e);
} catch (IllegalAccessException e) {
@@ -305,17 +281,26 @@
}
}
- @Override public ByteString getNpnSelectedProtocol(SSLSocket socket) {
+ @Override public void afterHandshake(SSLSocket sslSocket) {
try {
- JettyNpnProvider provider =
- (JettyNpnProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
+ removeMethod.invoke(null, sslSocket);
+ } catch (IllegalAccessException ignored) {
+ throw new AssertionError();
+ } catch (InvocationTargetException ignored) {
+ throw new AssertionError();
+ }
+ }
+
+ @Override public String getSelectedProtocol(SSLSocket socket) {
+ try {
+ JettyNegoProvider provider =
+ (JettyNegoProvider) Proxy.getInvocationHandler(getMethod.invoke(null, socket));
if (!provider.unsupported && provider.selected == null) {
- Logger logger = Logger.getLogger("com.squareup.okhttp.OkHttpClient");
- logger.log(Level.INFO,
- "NPN callback dropped so SPDY is disabled. Is npn-boot on the boot class path?");
+ logger.log(Level.INFO, "ALPN callback dropped: SPDY and HTTP/2 are disabled. "
+ + "Is alpn-boot on the boot class path?");
return null;
}
- return provider.unsupported ? null : ByteString.encodeUtf8(provider.selected);
+ return provider.unsupported ? null : provider.selected;
} catch (InvocationTargetException e) {
throw new AssertionError();
} catch (IllegalAccessException e) {
@@ -325,18 +310,18 @@
}
/**
- * Handle the methods of NextProtoNego's ClientProvider and ServerProvider
+ * Handle the methods of ALPN's ClientProvider and ServerProvider
* without a compile-time dependency on those interfaces.
*/
- private static class JettyNpnProvider implements InvocationHandler {
+ private static class JettyNegoProvider implements InvocationHandler {
/** This peer's supported protocols. */
private final List<String> protocols;
- /** Set when remote peer notifies NPN is unsupported. */
+ /** Set when remote peer notifies ALPN is unsupported. */
private boolean unsupported;
- /** The protocol the client selected. */
+ /** The protocol the server selected. */
private String selected;
- public JettyNpnProvider(List<String> protocols) {
+ public JettyNegoProvider(List<String> protocols) {
this.protocols = protocols;
}
@@ -347,27 +332,25 @@
args = Util.EMPTY_STRING_ARRAY;
}
if (methodName.equals("supports") && boolean.class == returnType) {
- return true; // Client supports NPN.
+ return true; // ALPN is supported.
} else if (methodName.equals("unsupported") && void.class == returnType) {
- this.unsupported = true; // Remote peer doesn't support NPN.
+ this.unsupported = true; // Peer doesn't support ALPN.
return null;
} else if (methodName.equals("protocols") && args.length == 0) {
- return protocols; // Server advertises these protocols.
- } else if (methodName.equals("selectProtocol") // Called when client.
- && String.class == returnType
- && args.length == 1
- && (args[0] == null || args[0] instanceof List)) {
- List<String> serverProtocols = (List) args[0];
- // Pick the first protocol the server advertises and client knows.
- for (int i = 0, size = serverProtocols.size(); i < size; i++) {
- if (protocols.contains(serverProtocols.get(i))) {
- return selected = serverProtocols.get(i);
+ return protocols; // Client advertises these protocols.
+ } else if ((methodName.equals("selectProtocol") || methodName.equals("select"))
+ && String.class == returnType && args.length == 1 && args[0] instanceof List) {
+ List<String> peerProtocols = (List) args[0];
+ // Pick the first known protocol the peer advertises.
+ for (int i = 0, size = peerProtocols.size(); i < size; i++) {
+ if (protocols.contains(peerProtocols.get(i))) {
+ return selected = peerProtocols.get(i);
}
}
- // On no intersection, try client's first protocol.
- return selected = protocols.get(0);
- } else if (methodName.equals("protocolSelected") && args.length == 1) {
- this.selected = (String) args[0]; // Client selected this protocol.
+ return selected = protocols.get(0); // On no intersection, try peer's first protocol.
+ } else if ((methodName.equals("protocolSelected") || methodName.equals("selected"))
+ && args.length == 1) {
+ this.selected = (String) args[0]; // Server selected this protocol.
return null;
} else {
return method.invoke(this, args);
@@ -376,24 +359,17 @@
}
/**
- * Concatenation of 8-bit, length prefixed protocol names.
- *
+ * Returns the concatenation of 8-bit, length prefixed protocol names.
* http://tools.ietf.org/html/draft-agl-tls-nextprotoneg-04#page-4
*/
static byte[] concatLengthPrefixed(List<Protocol> protocols) {
- int size = 0;
- for (Protocol protocol : protocols) {
- size += protocol.name.size() + 1; // add a byte for 8-bit length prefix.
+ Buffer result = new Buffer();
+ for (int i = 0, size = protocols.size(); i < size; i++) {
+ Protocol protocol = protocols.get(i);
+ if (protocol == Protocol.HTTP_1_0) continue; // No HTTP/1.0 for ALPN.
+ result.writeByte(protocol.toString().length());
+ result.writeUtf8(protocol.toString());
}
- byte[] result = new byte[size];
- int pos = 0;
- for (Protocol protocol : protocols) {
- int nameSize = protocol.name.size();
- result[pos++] = (byte) nameSize;
- // toByteArray allocates an array, but this is only called on new connections.
- System.arraycopy(protocol.name.toByteArray(), 0, result, pos, nameSize);
- pos += nameSize;
- }
- return result;
+ return result.readByteArray();
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java b/okhttp/src/main/java/com/squareup/okhttp/internal/RouteDatabase.java
similarity index 91%
rename from okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java
rename to okhttp/src/main/java/com/squareup/okhttp/internal/RouteDatabase.java
index 4177c0f..52c211e 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/RouteDatabase.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/RouteDatabase.java
@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp;
+package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Route;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -26,7 +27,7 @@
* preferred.
*/
public final class RouteDatabase {
- private final Set<Route> failedRoutes = new LinkedHashSet<Route>();
+ private final Set<Route> failedRoutes = new LinkedHashSet<>();
/** Records a failure connecting to {@code failedRoute}. */
public synchronized void failed(Route failedRoute) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java b/okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java
deleted file mode 100644
index c4e3047..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Copyright (C) 2014 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.util.Arrays;
-import javax.net.ssl.SSLSocket;
-
-/**
- * A configuration of desired secure socket protocols.
- */
-public class TlsConfiguration {
- private static final String SSL_V3 = "SSLv3";
- private static final String TLS_V1_2 = "TLSv1.2";
- private static final String TLS_V1_1 = "TLSv1.1";
- private static final String TLS_V1_0 = "TLSv1";
-
- public static final TlsConfiguration TLS_V1_2_AND_BELOW =
- new TlsConfiguration(new String[] { TLS_V1_2, TLS_V1_1, TLS_V1_0, SSL_V3 }, true);
- public static final TlsConfiguration TLS_V1_1_AND_BELOW =
- new TlsConfiguration(new String[] { TLS_V1_1, TLS_V1_0, SSL_V3 }, true);
- public static final TlsConfiguration TLS_V1_0_AND_BELOW =
- new TlsConfiguration(new String[] { TLS_V1_0, SSL_V3 }, true);
- public static final TlsConfiguration SSL_V3_ONLY =
- new TlsConfiguration(new String[] { SSL_V3 }, false /* supportsNpn */);
-
- // The set of all protocols. Can be null. If non-null it must have at least one item in it, which
- // must be supported. All others are considered optional.
- private final String[] protocols;
- private final boolean supportsNpn;
-
- /**
- * Creates a {@link TlsConfiguration} with the specified settings.
- *
- * <p>{@code protocols} must contain at least one value. The ordering of the protocols is
- * important: the first protocol specified <em>must</em> be enabled by a socket
- * for the {@link #isCompatible(javax.net.ssl.SSLSocket)} method to return {@code true}.
- * The other protocols are considered optional. {@code protocols} must not contain null values.
- */
- private TlsConfiguration(String[] protocols, boolean supportsNpn) {
- if (protocols == null || protocols.length == 0 || contains(protocols, null)) {
- throw new IllegalArgumentException(
- "protocols must contain at least one protocol and must not contain nulls");
- }
-
- this.protocols = protocols;
- this.supportsNpn = supportsNpn;
- }
-
- public boolean supportsNpn() {
- return supportsNpn;
- }
-
- /**
- * Returns {@code true} if the socket, as currently configured, supports this TLS configuration.
- */
- public boolean isCompatible(SSLSocket socket) {
- // We use enabled protocols here, not supported, to avoid re-enabling a protocol that has
- // been disabled. Just because something is supported does not make it desirable to use.
- String[] enabledProtocols = socket.getEnabledProtocols();
- return contains(enabledProtocols, protocols[0]);
- }
-
- public void configureProtocols(SSLSocket socket) {
- // We use enabled protocols here, not supported, to avoid re-enabling a protocol that has
- // been disabled. Just because something is supported does not make it desirable to use.
- String[] enabledProtocols = socket.getEnabledProtocols();
-
- // Create an array to hold the subset of protocols that are desired, and copy across the
- // enabled protocols that intersect.
- String[] desiredProtocols = new String[protocols.length];
- int desiredIndex = 0;
- for (String candidateProtocol : protocols) {
- if (contains(enabledProtocols, candidateProtocol)) {
- desiredProtocols[desiredIndex++] = candidateProtocol;
- } else if (desiredIndex == 0) {
- // This is checked by isCompatible.
- throw new AssertionError("primaryProtocol " + candidateProtocol + " is not supported");
- }
- }
-
- // Shrink the desiredProtocols array to the correct size.
- if (desiredIndex < desiredProtocols.length) {
- String[] desiredCopy = new String[desiredIndex];
- System.arraycopy(desiredProtocols, 0, desiredCopy, 0, desiredIndex);
- desiredProtocols = desiredCopy;
- }
-
- socket.setEnabledProtocols(desiredProtocols);
- }
-
- @Override
- public String toString() {
- return "TlsConfiguration{" +
- "protocols=" + Arrays.toString(protocols) +
- ", supportsNpn=" + supportsNpn +
- '}';
- }
-
- private static <T> boolean contains(T[] array, T value) {
- for (T arrayValue : array) {
- if (value == arrayValue || (value != null && value.equals(arrayValue))) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java
deleted file mode 100644
index c0f14a4..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2014 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.io.IOException;
-import java.security.cert.CertificateException;
-import java.util.Arrays;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLProtocolException;
-import javax.net.ssl.SSLSocket;
-
-/**
- * Handles the socket protocol connection fallback strategy: When a secure socket connection fails
- * due to a handshake / protocol problem the connection may be retried with different protocols.
- * Instances are stateful and should be created and used for a single connection attempt.
- */
-public class TlsFallbackStrategy {
-
- private final TlsConfiguration[] configurations;
- private int nextModeIndex;
- private boolean isFallbackPossible;
- private boolean isFallback;
-
- /** Create a new {@link TlsFallbackStrategy}. */
- public static TlsFallbackStrategy create() {
- return new TlsFallbackStrategy(
- TlsConfiguration.TLS_V1_2_AND_BELOW,
- TlsConfiguration.TLS_V1_1_AND_BELOW,
- TlsConfiguration.TLS_V1_0_AND_BELOW,
- TlsConfiguration.SSL_V3_ONLY);
- }
-
- /** Use {@link #create()} */
- private TlsFallbackStrategy(TlsConfiguration... configurations) {
- this.nextModeIndex = 0;
- this.configurations = configurations;
- }
-
- /**
- * Configure the supplied {@link SSLSocket} to connect to the specified host using an appropriate
- * {@link TlsConfiguration}.
- *
- * @return the chosen {@link TlsConfiguration}
- * @throws IOException if the socket does not support any of the tls modes available
- */
- public TlsConfiguration configureSecureSocket(SSLSocket sslSocket, String host, Platform platform)
- throws IOException {
-
- TlsConfiguration tlsConfiguration = null;
- for (int i = nextModeIndex; i < configurations.length; i++) {
- if (configurations[i].isCompatible(sslSocket)) {
- tlsConfiguration = configurations[i];
- nextModeIndex = i + 1;
- break;
- }
- }
-
- if (tlsConfiguration == null) {
- // This may be the first time a connection has been attempted and the socket does not support
- // any the required protocols, or it may be a retry (but this socket supports fewer
- // protocols than was suggested by a prior socket).
- throw new IOException(
- "Unable to find acceptable protocols. isFallback=" + isFallback +
- ", modes=" + Arrays.toString(configurations) +
- ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
- }
-
- isFallbackPossible = isFallbackPossible(sslSocket);
-
- tlsConfiguration.configureProtocols(sslSocket);
- platform.configureSecureSocket(sslSocket, host, isFallback);
- return tlsConfiguration;
- }
-
- /**
- * Reports a failure to complete a connection. Determines the next {@link TlsConfiguration} to
- * try, if any.
- *
- * @return {@code true} if the connection should be retried using
- * {@link #configureSecureSocket(SSLSocket, String, Platform)} or {@code false} if not
- */
- public boolean connectionFailed(IOException e) {
- // Any future attempt to connect using this strategy will be a fallback attempt.
- isFallback = true;
-
- if (e instanceof SSLHandshakeException) {
- // If the problem was a CertificateException from the X509TrustManager,
- // do not retry.
- if (e.getCause() instanceof CertificateException) {
- return false;
- }
- }
-
- // TODO(nfuller): should we retry SSLProtocolExceptions at all? SSLProtocolExceptions can be
- // caused by TLS_FALLBACK_SCSV failures, which means we retry those when we probably should not.
- return ((e instanceof SSLHandshakeException || e instanceof SSLProtocolException))
- && isFallbackPossible;
- }
-
- /**
- * Returns {@code true} if any later {@link TlsConfiguration} in the fallback strategy looks
- * possible based on the supplied {@link SSLSocket}. It assumes that a future socket will have the
- * same capabilities as the supplied socket.
- */
- private boolean isFallbackPossible(SSLSocket socket) {
- for (int i = nextModeIndex; i < configurations.length; i++) {
- if (configurations[i].isCompatible(socket)) {
- return true;
- }
- }
- return false;
- }
-}
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 51e04e8..5f8e8cb 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -16,15 +16,12 @@
package com.squareup.okhttp.internal;
-import com.squareup.okhttp.internal.spdy.Header;
-import java.io.ByteArrayInputStream;
import java.io.Closeable;
-import java.io.EOFException;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
+import java.io.InterruptedIOException;
import java.io.UnsupportedEncodingException;
+import java.lang.reflect.Array;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.URI;
@@ -35,22 +32,19 @@
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
import okio.ByteString;
-import okio.OkBuffer;
import okio.Source;
-import static java.util.concurrent.TimeUnit.NANOSECONDS;
-
/** Junk drawer of utility methods. */
public final class Util {
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
public static final String[] EMPTY_STRING_ARRAY = new String[0];
- public static final InputStream EMPTY_INPUT_STREAM = new ByteArrayInputStream(EMPTY_BYTE_ARRAY);
-
- /** A cheap and type-safe constant for the US-ASCII Charset. */
- public static final Charset US_ASCII = Charset.forName("US-ASCII");
/** A cheap and type-safe constant for the UTF-8 Charset. */
public static final Charset UTF_8 = Charset.forName("UTF-8");
@@ -175,93 +169,81 @@
}
/**
- * Fills 'dst' with bytes from 'in', throwing EOFException if insufficient bytes are available.
+ * Attempts to exhaust {@code source}, returning true if successful. This is useful when reading
+ * a complete source is helpful, such as when doing so completes a cache body or frees a socket
+ * connection for reuse.
*/
- public static void readFully(InputStream in, byte[] dst) throws IOException {
- readFully(in, dst, 0, dst.length);
+ public static boolean discard(Source source, int timeout, TimeUnit timeUnit) {
+ try {
+ return skipAll(source, timeout, timeUnit);
+ } catch (IOException e) {
+ return false;
+ }
}
/**
- * Reads exactly 'byteCount' bytes from 'in' (into 'dst' at offset 'offset'), and throws
- * EOFException if insufficient bytes are available.
- *
- * Used to implement {@link java.io.DataInputStream#readFully(byte[], int, int)}.
+ * Reads until {@code in} is exhausted or the deadline has been reached. This is careful to not
+ * extend the deadline if one exists already.
*/
- public static void readFully(InputStream in, byte[] dst, int offset, int byteCount)
- throws IOException {
- if (byteCount == 0) {
- return;
- }
- if (in == null) {
- throw new NullPointerException("in == null");
- }
- if (dst == null) {
- throw new NullPointerException("dst == null");
- }
- checkOffsetAndCount(dst.length, offset, byteCount);
- while (byteCount > 0) {
- int bytesRead = in.read(dst, offset, byteCount);
- if (bytesRead < 0) {
- throw new EOFException();
+ public static boolean skipAll(Source source, int duration, TimeUnit timeUnit) throws IOException {
+ long now = System.nanoTime();
+ long originalDuration = source.timeout().hasDeadline()
+ ? source.timeout().deadlineNanoTime() - now
+ : Long.MAX_VALUE;
+ source.timeout().deadlineNanoTime(now + Math.min(originalDuration, timeUnit.toNanos(duration)));
+ try {
+ Buffer skipBuffer = new Buffer();
+ while (source.read(skipBuffer, 2048) != -1) {
+ skipBuffer.clear();
}
- offset += bytesRead;
- byteCount -= bytesRead;
+ return true; // Success! The source has been exhausted.
+ } catch (InterruptedIOException e) {
+ return false; // We ran out of time before exhausting the source.
+ } finally {
+ if (originalDuration == Long.MAX_VALUE) {
+ source.timeout().clearDeadline();
+ } else {
+ source.timeout().deadlineNanoTime(now + originalDuration);
+ }
}
}
- /** Returns the remainder of 'source' as a buffer, closing it when done. */
- public static OkBuffer readFully(Source source) throws IOException {
- OkBuffer result = new OkBuffer();
- while (source.read(result, 2048) != -1) {
- }
- source.close();
- return result;
- }
-
- /** Reads until {@code in} is exhausted or the timeout has elapsed. */
- public static boolean skipAll(Source in, int timeoutMillis) throws IOException {
- // TODO: Implement deadlines everywhere so they can do this work.
- long startNanos = System.nanoTime();
- OkBuffer skipBuffer = new OkBuffer();
- while (NANOSECONDS.toMillis(System.nanoTime() - startNanos) < timeoutMillis) {
- long read = in.read(skipBuffer, 2048);
- if (read == -1) return true; // Successfully exhausted the stream.
- skipBuffer.clear();
- }
- return false; // Ran out of time.
- }
-
- /**
- * Copies all of the bytes from {@code in} to {@code out}. Neither stream is closed.
- * Returns the total number of bytes transferred.
- */
- public static int copy(InputStream in, OutputStream out) throws IOException {
- int total = 0;
- byte[] buffer = new byte[8192];
- int c;
- while ((c = in.read(buffer)) != -1) {
- total += c;
- out.write(buffer, 0, c);
- }
- return total;
- }
-
- /** Returns a 32 character string containing a hash of {@code s}. */
- public static String hash(String s) {
+ /** Returns a 32 character string containing an MD5 hash of {@code s}. */
+ public static String md5Hex(String s) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
byte[] md5bytes = messageDigest.digest(s.getBytes("UTF-8"));
return ByteString.of(md5bytes).hex();
- } catch (NoSuchAlgorithmException e) {
+ } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
throw new AssertionError(e);
- } catch (UnsupportedEncodingException e) {
+ }
+ }
+
+ /** Returns a Base 64-encoded string containing a SHA-1 hash of {@code s}. */
+ public static String shaBase64(String s) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+ byte[] sha1Bytes = messageDigest.digest(s.getBytes("UTF-8"));
+ return ByteString.of(sha1Bytes).base64();
+ } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Returns a SHA-1 hash of {@code s}. */
+ public static ByteString sha1(ByteString s) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance("SHA-1");
+ byte[] sha1Bytes = messageDigest.digest(s.toByteArray());
+ return ByteString.of(sha1Bytes);
+ } catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
/** Returns an immutable copy of {@code list}. */
public static <T> List<T> immutableList(List<T> list) {
- return Collections.unmodifiableList(new ArrayList<T>(list));
+ return Collections.unmodifiableList(new ArrayList<>(list));
}
/** Returns an immutable list containing {@code elements}. */
@@ -269,6 +251,11 @@
return Collections.unmodifiableList(Arrays.asList(elements.clone()));
}
+ /** Returns an immutable copy of {@code map}. */
+ public static <K, V> Map<K, V> immutableMap(Map<K, V> map) {
+ return Collections.unmodifiableMap(new LinkedHashMap<>(map));
+ }
+
public static ThreadFactory threadFactory(final String name, final boolean daemon) {
return new ThreadFactory() {
@Override public Thread newThread(Runnable runnable) {
@@ -279,10 +266,29 @@
};
}
- public static List<Header> headerEntries(String... elements) {
- List<Header> result = new ArrayList<Header>(elements.length / 2);
- for (int i = 0; i < elements.length; i += 2) {
- result.add(new Header(elements[i], elements[i + 1]));
+ /**
+ * Returns an array containing containing only elements found in {@code first} and also in
+ * {@code second}. The returned elements are in the same order as in {@code first}.
+ */
+ @SuppressWarnings("unchecked")
+ public static <T> T[] intersect(Class<T> arrayType, T[] first, T[] second) {
+ List<T> result = intersect(first, second);
+ return result.toArray((T[]) Array.newInstance(arrayType, result.size()));
+ }
+
+ /**
+ * Returns a list containing containing only elements found in {@code first} and also in
+ * {@code second}. The returned elements are in the same order as in {@code first}.
+ */
+ private static <T> List<T> intersect(T[] first, T[] second) {
+ List<T> result = new ArrayList<>();
+ for (T a : first) {
+ for (T b : second) {
+ if (a.equals(b)) {
+ result.add(b);
+ break;
+ }
+ }
}
return result;
}
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
new file mode 100644
index 0000000..a517ada
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/AuthenticatorAdapter.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Authenticator;
+import com.squareup.okhttp.Challenge;
+import com.squareup.okhttp.Credentials;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.net.Authenticator.RequestorType;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.PasswordAuthentication;
+import java.net.Proxy;
+import java.net.URL;
+import java.util.List;
+
+/** Adapts {@link java.net.Authenticator} to {@link com.squareup.okhttp.Authenticator}. */
+public final class AuthenticatorAdapter implements Authenticator {
+ /** Uses the global authenticator to get the password. */
+ public static final Authenticator INSTANCE = new AuthenticatorAdapter();
+
+ @Override public Request authenticate(Proxy proxy, Response response) throws IOException {
+ List<Challenge> challenges = response.challenges();
+ Request request = response.request();
+ URL url = request.url();
+ for (int i = 0, size = challenges.size(); i < size; i++) {
+ Challenge challenge = challenges.get(i);
+ if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue;
+
+ PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
+ url.getHost(), getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
+ challenge.getRealm(), challenge.getScheme(), url, RequestorType.SERVER);
+ if (auth == null) continue;
+
+ String credential = Credentials.basic(auth.getUserName(), new String(auth.getPassword()));
+ return request.newBuilder()
+ .header("Authorization", credential)
+ .build();
+ }
+ return null;
+
+ }
+
+ @Override public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
+ List<Challenge> challenges = response.challenges();
+ Request request = response.request();
+ URL url = request.url();
+ for (int i = 0, size = challenges.size(); i < size; i++) {
+ Challenge challenge = challenges.get(i);
+ if (!"Basic".equalsIgnoreCase(challenge.getScheme())) continue;
+
+ InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+ PasswordAuthentication auth = java.net.Authenticator.requestPasswordAuthentication(
+ proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
+ url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
+ RequestorType.PROXY);
+ if (auth == null) continue;
+
+ String credential = Credentials.basic(auth.getUserName(), new String(auth.getPassword()));
+ return request.newBuilder()
+ .header("Proxy-Authorization", credential)
+ .build();
+ }
+ return null;
+ }
+
+ private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
+ return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
+ ? ((InetSocketAddress) proxy.address()).getAddress()
+ : InetAddress.getByName(url.getHost());
+ }
+}
diff --git a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheRequest.java
similarity index 75%
copy from okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
copy to okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheRequest.java
index ac3de72..b8153e4 100644
--- a/okio/src/test/java/okio/OkBufferReadUtf8LineTest.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheRequest.java
@@ -13,10 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package okio;
+package com.squareup.okhttp.internal.http;
-public final class OkBufferReadUtf8LineTest extends ReadUtf8LineTest {
- @Override protected BufferedSource newSource(String s) {
- return new OkBuffer().writeUtf8(s);
- }
+import java.io.IOException;
+import okio.Sink;
+
+public interface CacheRequest {
+ Sink body() throws IOException;
+ void abort();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
index 75e13d9..7af04aa 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/CacheStrategy.java
@@ -1,16 +1,25 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.CacheControl;
-import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseSource;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
+
import java.util.Date;
-import static com.squareup.okhttp.internal.Util.EMPTY_INPUT_STREAM;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
+import static java.net.HttpURLConnection.HTTP_BAD_METHOD;
+import static java.net.HttpURLConnection.HTTP_GONE;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
+import static java.net.HttpURLConnection.HTTP_NOT_AUTHORITATIVE;
+import static java.net.HttpURLConnection.HTTP_NOT_FOUND;
+import static java.net.HttpURLConnection.HTTP_NOT_IMPLEMENTED;
+import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_REQ_TOO_LONG;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
@@ -22,76 +31,57 @@
* response (if the cached data is potentially stale).
*/
public final class CacheStrategy {
- private static final Response.Body EMPTY_BODY = new Response.Body() {
- @Override public boolean ready() throws IOException {
- return true;
- }
- @Override public MediaType contentType() {
- return null;
- }
- @Override public long contentLength() {
- return 0;
- }
- @Override public InputStream byteStream() {
- return EMPTY_INPUT_STREAM;
- }
- };
-
- private static final StatusLine GATEWAY_TIMEOUT_STATUS_LINE;
- static {
- try {
- GATEWAY_TIMEOUT_STATUS_LINE = new StatusLine("HTTP/1.1 504 Gateway Timeout");
- } catch (IOException e) {
- throw new AssertionError();
- }
- }
-
/** The request to send on the network, or null if this call doesn't use the network. */
public final Request networkRequest;
/** The cached response to return or validate; or null if this call doesn't use a cache. */
public final Response cacheResponse;
- public final ResponseSource source;
-
- private CacheStrategy(
- Request networkRequest, Response cacheResponse, ResponseSource source) {
+ private CacheStrategy(Request networkRequest, Response cacheResponse) {
this.networkRequest = networkRequest;
this.cacheResponse = cacheResponse;
- this.source = source;
}
/**
- * Returns true if this response can be stored to later serve another
+ * Returns true if {@code response} can be stored to later serve another
* request.
*/
public static boolean isCacheable(Response response, Request request) {
- // Always go to network for uncacheable response codes (RFC 2616, 13.4),
+ // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
// This implementation doesn't support caching partial content.
- int responseCode = response.code();
- if (responseCode != HttpURLConnection.HTTP_OK
- && responseCode != HttpURLConnection.HTTP_NOT_AUTHORITATIVE
- && responseCode != HttpURLConnection.HTTP_MULT_CHOICE
- && responseCode != HttpURLConnection.HTTP_MOVED_PERM
- && responseCode != HttpURLConnection.HTTP_GONE) {
- return false;
+ switch (response.code()) {
+ case HTTP_OK:
+ case HTTP_NOT_AUTHORITATIVE:
+ case HTTP_NO_CONTENT:
+ case HTTP_MULT_CHOICE:
+ case HTTP_MOVED_PERM:
+ case HTTP_NOT_FOUND:
+ case HTTP_BAD_METHOD:
+ case HTTP_GONE:
+ case HTTP_REQ_TOO_LONG:
+ case HTTP_NOT_IMPLEMENTED:
+ case HTTP_PERM_REDIRECT:
+ // These codes can be cached unless headers forbid it.
+ break;
+
+ case HTTP_MOVED_TEMP:
+ case HTTP_TEMP_REDIRECT:
+ // These codes can only be cached with the right response headers.
+ if (response.header("Expires") != null
+ || response.cacheControl().maxAgeSeconds() != -1
+ || response.cacheControl().sMaxAgeSeconds() != -1
+ || response.cacheControl().isPublic()) {
+ break;
+ }
+ // Fall-through.
+
+ default:
+ // All other codes cannot be cached.
+ return false;
}
- // Responses to authorized requests aren't cacheable unless they include
- // a 'public', 'must-revalidate' or 's-maxage' directive.
- CacheControl responseCaching = response.cacheControl();
- if (request.header("Authorization") != null
- && !responseCaching.isPublic()
- && !responseCaching.mustRevalidate()
- && responseCaching.sMaxAgeSeconds() == -1) {
- return false;
- }
-
- if (responseCaching.noStore()) {
- return false;
- }
-
- return true;
+ // A 'no-store' directive on request or response prevents the response from being cached.
+ return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}
public static class Factory {
@@ -137,9 +127,10 @@
this.cacheResponse = cacheResponse;
if (cacheResponse != null) {
- for (int i = 0; i < cacheResponse.headers().size(); i++) {
- String fieldName = cacheResponse.headers().name(i);
- String value = cacheResponse.headers().value(i);
+ Headers headers = cacheResponse.headers();
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ String fieldName = headers.name(i);
+ String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {
servedDate = HttpDate.parse(value);
servedDateString = value;
@@ -151,7 +142,7 @@
} else if ("ETag".equalsIgnoreCase(fieldName)) {
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {
- ageSeconds = HeaderParser.parseSeconds(value);
+ ageSeconds = HeaderParser.parseSeconds(value, -1);
} else if (OkHeaders.SENT_MILLIS.equalsIgnoreCase(fieldName)) {
sentRequestMillis = Long.parseLong(value);
} else if (OkHeaders.RECEIVED_MILLIS.equalsIgnoreCase(fieldName)) {
@@ -168,15 +159,9 @@
public CacheStrategy get() {
CacheStrategy candidate = getCandidate();
- if (candidate.source != ResponseSource.CACHE && request.cacheControl().onlyIfCached()) {
- // We're forbidden from using the network, but the cache is insufficient.
- Response noneResponse = new Response.Builder()
- .request(candidate.networkRequest)
- .statusLine(GATEWAY_TIMEOUT_STATUS_LINE)
- .setResponseSource(ResponseSource.NONE)
- .body(EMPTY_BODY)
- .build();
- return new CacheStrategy(null, noneResponse, ResponseSource.NONE);
+ if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {
+ // We're forbidden from using the network and the cache is insufficient.
+ return new CacheStrategy(null, null);
}
return candidate;
@@ -186,24 +171,24 @@
private CacheStrategy getCandidate() {
// No cached response.
if (cacheResponse == null) {
- return new CacheStrategy(request, null, ResponseSource.NETWORK);
+ return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
if (request.isHttps() && cacheResponse.handshake() == null) {
- return new CacheStrategy(request, null, ResponseSource.NETWORK);
+ return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
if (!isCacheable(cacheResponse, request)) {
- return new CacheStrategy(request, null, ResponseSource.NETWORK);
+ return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
- return new CacheStrategy(request, cacheResponse, ResponseSource.NETWORK);
+ return new CacheStrategy(request, null);
}
long ageMillis = cacheResponseAge();
@@ -225,8 +210,7 @@
}
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
- Response.Builder builder = cacheResponse.newBuilder()
- .setResponseSource(ResponseSource.CACHE); // Overwrite any stored response source.
+ Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
@@ -234,7 +218,7 @@
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
- return new CacheStrategy(null, builder.build(), ResponseSource.CACHE);
+ return new CacheStrategy(null, builder.build());
}
Request.Builder conditionalRequestBuilder = request.newBuilder();
@@ -251,8 +235,8 @@
Request conditionalRequest = conditionalRequestBuilder.build();
return hasConditions(conditionalRequest)
- ? new CacheStrategy(conditionalRequest, cacheResponse, ResponseSource.CONDITIONAL_CACHE)
- : new CacheStrategy(conditionalRequest, null, ResponseSource.NETWORK);
+ ? new CacheStrategy(conditionalRequest, cacheResponse)
+ : new CacheStrategy(conditionalRequest, null);
}
/**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
index e9af130..55f82ad 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HeaderParser.java
@@ -47,9 +47,9 @@
/**
* Returns {@code value} as a positive integer, or 0 if it is negative, or
- * -1 if it cannot be parsed.
+ * {@code defaultValue} if it cannot be parsed.
*/
- public static int parseSeconds(String value) {
+ public static int parseSeconds(String value, int defaultValue) {
try {
long seconds = Long.parseLong(value);
if (seconds > Integer.MAX_VALUE) {
@@ -60,7 +60,7 @@
return (int) seconds;
}
} catch (NumberFormatException e) {
- return -1;
+ return defaultValue;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
deleted file mode 100644
index ce40a92..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpAuthenticator.java
+++ /dev/null
@@ -1,167 +0,0 @@
-/*
- * Copyright (C) 2012 Square, Inc.
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.Headers;
-import com.squareup.okhttp.OkAuthenticator;
-import com.squareup.okhttp.OkAuthenticator.Challenge;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import java.io.IOException;
-import java.net.Authenticator;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.net.PasswordAuthentication;
-import java.net.Proxy;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.squareup.okhttp.OkAuthenticator.Credential;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
-import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
-
-/** Handles HTTP authentication headers from origin and proxy servers. */
-public final class HttpAuthenticator {
- /** Uses the global authenticator to get the password. */
- public static final OkAuthenticator SYSTEM_DEFAULT = new OkAuthenticator() {
- @Override public Credential authenticate(
- Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
- for (int i = 0, size = challenges.size(); i < size; i++) {
- Challenge challenge = challenges.get(i);
- if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
- continue;
- }
-
- PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(url.getHost(),
- getConnectToInetAddress(proxy, url), url.getPort(), url.getProtocol(),
- challenge.getRealm(), challenge.getScheme(), url, Authenticator.RequestorType.SERVER);
- if (auth != null) {
- return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
- }
- }
- return null;
- }
-
- @Override public Credential authenticateProxy(
- Proxy proxy, URL url, List<Challenge> challenges) throws IOException {
- for (int i = 0, size = challenges.size(); i < size; i++) {
- Challenge challenge = challenges.get(i);
- if (!"Basic".equalsIgnoreCase(challenge.getScheme())) {
- continue;
- }
-
- InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
- PasswordAuthentication auth = Authenticator.requestPasswordAuthentication(
- proxyAddress.getHostName(), getConnectToInetAddress(proxy, url), proxyAddress.getPort(),
- url.getProtocol(), challenge.getRealm(), challenge.getScheme(), url,
- Authenticator.RequestorType.PROXY);
- if (auth != null) {
- return Credential.basic(auth.getUserName(), new String(auth.getPassword()));
- }
- }
- return null;
- }
-
- private InetAddress getConnectToInetAddress(Proxy proxy, URL url) throws IOException {
- return (proxy != null && proxy.type() != Proxy.Type.DIRECT)
- ? ((InetSocketAddress) proxy.address()).getAddress()
- : InetAddress.getByName(url.getHost());
- }
- };
-
- private HttpAuthenticator() {
- }
-
- /**
- * React to a failed authorization response by looking up new credentials.
- * Returns a request for a subsequent attempt, or null if no further attempts
- * should be made.
- */
- public static Request processAuthHeader(
- OkAuthenticator authenticator, Response response, Proxy proxy) throws IOException {
- String responseField;
- String requestField;
- if (response.code() == HTTP_UNAUTHORIZED) {
- responseField = "WWW-Authenticate";
- requestField = "Authorization";
- } else if (response.code() == HTTP_PROXY_AUTH) {
- responseField = "Proxy-Authenticate";
- requestField = "Proxy-Authorization";
- } else {
- throw new IllegalArgumentException(); // TODO: ProtocolException?
- }
- List<Challenge> challenges = parseChallenges(response.headers(), responseField);
- if (challenges.isEmpty()) return null; // Could not find a challenge so end the request cycle.
-
- Request request = response.request();
- Credential credential = response.code() == HTTP_PROXY_AUTH
- ? authenticator.authenticateProxy(proxy, request.url(), challenges)
- : authenticator.authenticate(proxy, request.url(), challenges);
- if (credential == null) return null; // Couldn't satisfy the challenge so end the request cycle.
-
- // Add authorization credentials, bypassing the already-connected check.
- return request.newBuilder().header(requestField, credential.getHeaderValue()).build();
- }
-
- /**
- * Parse RFC 2617 challenges. This API is only interested in the scheme
- * name and realm.
- */
- private static List<Challenge> parseChallenges(Headers responseHeaders,
- String challengeHeader) {
- // auth-scheme = token
- // auth-param = token "=" ( token | quoted-string )
- // challenge = auth-scheme 1*SP 1#auth-param
- // realm = "realm" "=" realm-value
- // realm-value = quoted-string
- List<Challenge> result = new ArrayList<Challenge>();
- for (int h = 0; h < responseHeaders.size(); h++) {
- if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(h))) {
- continue;
- }
- String value = responseHeaders.value(h);
- int pos = 0;
- while (pos < value.length()) {
- int tokenStart = pos;
- pos = HeaderParser.skipUntil(value, pos, " ");
-
- String scheme = value.substring(tokenStart, pos).trim();
- pos = HeaderParser.skipWhitespace(value, pos);
-
- // TODO: This currently only handles schemes with a 'realm' parameter;
- // It needs to be fixed to handle any scheme and any parameters
- // http://code.google.com/p/android/issues/detail?id=11140
-
- if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) {
- break; // Unexpected challenge parameter; give up!
- }
-
- pos += "realm=\"".length();
- int realmStart = pos;
- pos = HeaderParser.skipUntil(value, pos, "\"");
- String realm = value.substring(realmStart, pos);
- pos++; // Consume '"' close quote.
- pos = HeaderParser.skipUntil(value, pos, ",");
- pos++; // Consume ',' comma.
- pos = HeaderParser.skipWhitespace(value, pos);
- result.add(new Challenge(scheme, realm));
- }
- }
- return result;
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
index 718d471..d4743d2 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
@@ -19,26 +19,25 @@
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Headers;
-import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
-import java.io.OutputStream;
-import java.net.CacheRequest;
import java.net.ProtocolException;
import java.net.Socket;
import java.net.SocketTimeoutException;
+import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
-import okio.Deadline;
-import okio.OkBuffer;
import okio.Okio;
import okio.Sink;
import okio.Source;
+import okio.Timeout;
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;
/**
* A socket connection that can be used to send HTTP/1.1 messages. This class
@@ -47,16 +46,17 @@
* <li>{@link #writeRequest Send request headers}.
* <li>Open a sink to write the request body. Either {@link
* #newFixedLengthSink fixed-length} or {@link #newChunkedSink chunked}.
- * <li>Write to and then close that stream.
+ * <li>Write to and then close that sink.
* <li>{@link #readResponse Read response headers}.
- * <li>Open the HTTP response body input stream. Either {@link
+ * <li>Open a source to read the response body. Either {@link
* #newFixedLengthSource fixed-length}, {@link #newChunkedSource chunked}
* or {@link #newUnknownLengthSource unknown length}.
- * <li>Read from and close that stream.
+ * <li>Read from and close that source.
* </ol>
* <p>Exchanges that do not have a request body may skip creating and closing
- * the request body. Exchanges that do not have a response body must call {@link
- * #emptyResponseBody}.
+ * the request body. Exchanges that do not have a response body can call {@link
+ * #newFixedLengthSource(long) newFixedLengthSource(0)} and may skip reading and
+ * closing that source.
*/
public final class HttpConnection {
private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
@@ -85,8 +85,17 @@
this.pool = pool;
this.connection = connection;
this.socket = socket;
- this.source = Okio.buffer(Okio.source(socket.getInputStream()));
- this.sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
+ this.source = Okio.buffer(Okio.source(socket));
+ this.sink = Okio.buffer(Okio.sink(socket));
+ }
+
+ public void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) {
+ if (readTimeoutMillis != 0) {
+ source.timeout().timeout(readTimeoutMillis, MILLISECONDS);
+ }
+ if (writeTimeoutMillis != 0) {
+ sink.timeout().timeout(writeTimeoutMillis, MILLISECONDS);
+ }
}
/**
@@ -99,7 +108,7 @@
// 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.
- pool.recycle(connection);
+ Internal.instance.recycle(pool, connection);
}
}
@@ -113,7 +122,7 @@
// If we're already idle, close immediately.
if (state == STATE_IDLE) {
state = STATE_CLOSED;
- connection.close();
+ connection.getSocket().close();
}
}
@@ -123,7 +132,7 @@
}
public void closeIfOwnedBy(Object owner) throws IOException {
- connection.closeIfOwnedBy(owner);
+ Internal.instance.closeIfOwnedBy(connection, owner);
}
public void flush() throws IOException {
@@ -159,7 +168,7 @@
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
sink.writeUtf8(requestLine).writeUtf8("\r\n");
- for (int i = 0; i < headers.size(); i ++) {
+ for (int i = 0, size = headers.size(); i < size; i ++) {
sink.writeUtf8(headers.name(i))
.writeUtf8(": ")
.writeUtf8(headers.value(i))
@@ -176,18 +185,19 @@
}
while (true) {
- String statusLineString = source.readUtf8LineStrict();
- StatusLine statusLine = new StatusLine(statusLineString);
+ StatusLine statusLine = StatusLine.parse(source.readUtf8LineStrict());
Response.Builder responseBuilder = new Response.Builder()
- .statusLine(statusLine)
- .header(OkHeaders.SELECTED_PROTOCOL, Protocol.HTTP_11.name.utf8());
+ .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());
- if (statusLine.code() != HTTP_CONTINUE) {
+ if (statusLine.code != HTTP_CONTINUE) {
state = STATE_OPEN_RESPONSE_BODY;
return responseBuilder;
}
@@ -198,27 +208,7 @@
public void readHeaders(Headers.Builder builder) throws IOException {
// parse the result headers until the first blank line
for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
- builder.addLine(line);
- }
- }
-
- /**
- * Discards the response body so that the connection can be reused and the
- * cache entry can be completed. This needs to be done judiciously, since it
- * delays the current request in order to speed up a potential future request
- * that may never occur.
- */
- public boolean discard(Source in, int timeoutMillis) {
- try {
- int socketTimeout = socket.getSoTimeout();
- socket.setSoTimeout(timeoutMillis);
- try {
- return Util.skipAll(in, timeoutMillis);
- } finally {
- socket.setSoTimeout(socketTimeout);
- }
- } catch (IOException e) {
- return false;
+ Internal.instance.addLenient(builder, line);
}
}
@@ -240,32 +230,22 @@
requestBody.writeToSocket(sink);
}
- public Source newFixedLengthSource(CacheRequest cacheRequest, long length)
- throws IOException {
+ public Source newFixedLengthSource(long length) throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
- return new FixedLengthSource(cacheRequest, length);
+ return new FixedLengthSource(length);
}
- /**
- * Call this to advance past a response body for HTTP responses that do not
- * have a response body.
- */
- public void emptyResponseBody() throws IOException {
- newFixedLengthSource(null, 0L); // Transition to STATE_IDLE.
- }
-
- public Source newChunkedSource(CacheRequest cacheRequest, HttpEngine httpEngine)
- throws IOException {
+ public Source newChunkedSource(HttpEngine httpEngine) throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
- return new ChunkedSource(cacheRequest, httpEngine);
+ return new ChunkedSource(httpEngine);
}
- public Source newUnknownLengthSource(CacheRequest cacheRequest) throws IOException {
+ public Source newUnknownLengthSource() throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READING_RESPONSE_BODY;
- return new UnknownLengthSource(cacheRequest);
+ return new UnknownLengthSource();
}
/** An HTTP body with a fixed length known in advance. */
@@ -277,11 +257,11 @@
this.bytesRemaining = bytesRemaining;
}
- @Override public Sink deadline(Deadline deadline) {
- return this; // TODO: honor deadline.
+ @Override public Timeout timeout() {
+ return sink.timeout();
}
- @Override public void write(OkBuffer source, long byteCount) throws IOException {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
checkOffsetAndCount(source.size(), 0, byteCount);
if (byteCount > bytesRemaining) {
@@ -305,11 +285,11 @@
}
}
- private static final String CRLF = "\r\n";
+ private static final byte[] CRLF = { '\r', '\n' };
private static final byte[] HEX_DIGITS = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
};
- private static final byte[] FINAL_CHUNK = new byte[] { '0', '\r', '\n', '\r', '\n' };
+ private static final byte[] FINAL_CHUNK = { '0', '\r', '\n', '\r', '\n' };
/**
* An HTTP body with alternating chunk sizes and chunk bodies. It is the
@@ -322,17 +302,17 @@
private boolean closed;
- @Override public Sink deadline(Deadline deadline) {
- return this; // TODO: honor deadline.
+ @Override public Timeout timeout() {
+ return sink.timeout();
}
- @Override public void write(OkBuffer source, long byteCount) throws IOException {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
if (byteCount == 0) return;
writeHex(byteCount);
sink.write(source, byteCount);
- sink.writeUtf8(CRLF);
+ sink.write(CRLF);
}
@Override public synchronized void flush() throws IOException {
@@ -360,27 +340,11 @@
}
}
- private class AbstractSource {
- private final CacheRequest cacheRequest;
- protected final OutputStream cacheBody;
+ private abstract class AbstractSource implements Source {
protected boolean closed;
- AbstractSource(CacheRequest cacheRequest) throws IOException {
- // Some apps return a null body; for compatibility we treat that like a null cache request.
- OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
- if (cacheBody == null) {
- cacheRequest = null;
- }
-
- this.cacheBody = cacheBody;
- this.cacheRequest = cacheRequest;
- }
-
- /** Copy the last {@code byteCount} bytes of {@code source} to the cache body. */
- protected final void cacheWrite(OkBuffer source, long byteCount) throws IOException {
- if (cacheBody != null) {
- Okio.copy(source, source.size() - byteCount, byteCount, cacheBody);
- }
+ @Override public Timeout timeout() {
+ return source.timeout();
}
/**
@@ -390,17 +354,13 @@
protected final void endOfInput(boolean recyclable) throws IOException {
if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
- if (cacheRequest != null) {
- cacheBody.close();
- }
-
state = STATE_IDLE;
if (recyclable && onIdle == ON_IDLE_POOL) {
onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
- pool.recycle(connection);
+ Internal.instance.recycle(pool, connection);
} else if (onIdle == ON_IDLE_CLOSE) {
state = STATE_CLOSED;
- connection.close();
+ connection.getSocket().close();
}
}
@@ -417,55 +377,45 @@
* to cancel the transfer, closing the connection is the only solution.
*/
protected final void unexpectedEndOfInput() {
- if (cacheRequest != null) {
- cacheRequest.abort();
- }
- Util.closeQuietly(connection);
+ Util.closeQuietly(connection.getSocket());
state = STATE_CLOSED;
}
}
/** An HTTP body with a fixed length specified in advance. */
- private class FixedLengthSource extends AbstractSource implements Source {
+ private class FixedLengthSource extends AbstractSource {
private long bytesRemaining;
- public FixedLengthSource(CacheRequest cacheRequest, long length) throws IOException {
- super(cacheRequest);
+ public FixedLengthSource(long length) throws IOException {
bytesRemaining = length;
if (bytesRemaining == 0) {
endOfInput(true);
}
}
- @Override public long read(OkBuffer sink, long byteCount)
- throws IOException {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (bytesRemaining == 0) return -1;
long read = source.read(sink, Math.min(bytesRemaining, byteCount));
if (read == -1) {
- unexpectedEndOfInput(); // the server didn't supply the promised content length
+ unexpectedEndOfInput(); // The server didn't supply the promised content length.
throw new ProtocolException("unexpected end of stream");
}
bytesRemaining -= read;
- cacheWrite(sink, read);
if (bytesRemaining == 0) {
endOfInput(true);
}
return read;
}
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
-
@Override public void close() throws IOException {
if (closed) return;
- if (bytesRemaining != 0 && !discard(this, DISCARD_STREAM_TIMEOUT_MILLIS)) {
+ if (bytesRemaining != 0
+ && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
unexpectedEndOfInput();
}
@@ -474,19 +424,17 @@
}
/** An HTTP body with alternating chunk sizes and chunk bodies. */
- private class ChunkedSource extends AbstractSource implements Source {
+ private class ChunkedSource extends AbstractSource {
private static final int NO_CHUNK_YET = -1;
private int bytesRemainingInChunk = NO_CHUNK_YET;
private boolean hasMoreChunks = true;
private final HttpEngine httpEngine;
- ChunkedSource(CacheRequest cacheRequest, HttpEngine httpEngine) throws IOException {
- super(cacheRequest);
+ ChunkedSource(HttpEngine httpEngine) throws IOException {
this.httpEngine = httpEngine;
}
- @Override public long read(
- OkBuffer sink, long byteCount) throws IOException {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (!hasMoreChunks) return -1;
@@ -498,16 +446,15 @@
long read = source.read(sink, Math.min(byteCount, bytesRemainingInChunk));
if (read == -1) {
- unexpectedEndOfInput(); // the server didn't supply the promised chunk length
+ unexpectedEndOfInput(); // The server didn't supply the promised chunk length.
throw new IOException("unexpected end of stream");
}
bytesRemainingInChunk -= read;
- cacheWrite(sink, read);
return read;
}
private void readChunkSize() throws IOException {
- // read the suffix of the previous chunk
+ // Read the suffix of the previous chunk.
if (bytesRemainingInChunk != NO_CHUNK_YET) {
source.readUtf8LineStrict();
}
@@ -530,14 +477,9 @@
}
}
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
-
@Override public void close() throws IOException {
if (closed) return;
- if (hasMoreChunks && !discard(this, DISCARD_STREAM_TIMEOUT_MILLIS)) {
+ if (hasMoreChunks && !Util.discard(this, DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
unexpectedEndOfInput();
}
closed = true;
@@ -545,14 +487,10 @@
}
/** An HTTP message body terminated by the end of the underlying stream. */
- class UnknownLengthSource extends AbstractSource implements Source {
+ private class UnknownLengthSource extends AbstractSource {
private boolean inputExhausted;
- UnknownLengthSource(CacheRequest cacheRequest) throws IOException {
- super(cacheRequest);
- }
-
- @Override public long read(OkBuffer sink, long byteCount)
+ @Override public long read(Buffer sink, long byteCount)
throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
@@ -564,18 +502,11 @@
endOfInput(false);
return -1;
}
- cacheWrite(sink, read);
return read;
}
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
-
@Override public void close() throws IOException {
if (closed) return;
- // TODO: discard unknown length streams for best caching?
if (!inputExhausted) {
unexpectedEndOfInput();
}
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 0013226..643fa09 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
@@ -18,40 +18,61 @@
package com.squareup.okhttp.internal.http;
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.Interceptor;
+import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.OkResponseCache;
+import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseSource;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.Route;
-import com.squareup.okhttp.TunnelRequest;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.InternalCache;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.Version;
import java.io.IOException;
-import java.io.InputStream;
-import java.net.CacheRequest;
+import java.io.InterruptedIOException;
import java.net.CookieHandler;
import java.net.ProtocolException;
+import java.net.Proxy;
import java.net.URL;
import java.net.UnknownHostException;
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;
+import okio.BufferedSource;
import okio.GzipSource;
import okio.Okio;
import okio.Sink;
import okio.Source;
+import okio.Timeout;
import static com.squareup.okhttp.internal.Util.closeQuietly;
import static com.squareup.okhttp.internal.Util.getDefaultPort;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
import static com.squareup.okhttp.internal.http.StatusLine.HTTP_CONTINUE;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_PERM_REDIRECT;
+import static com.squareup.okhttp.internal.http.StatusLine.HTTP_TEMP_REDIRECT;
+import static java.net.HttpURLConnection.HTTP_MOVED_PERM;
+import static java.net.HttpURLConnection.HTTP_MOVED_TEMP;
+import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED;
import static java.net.HttpURLConnection.HTTP_NO_CONTENT;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.net.HttpURLConnection.HTTP_SEE_OTHER;
+import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* Handles a single HTTP request/response pair. Each HTTP engine follows this
@@ -71,10 +92,29 @@
* <p>The request and response may be served by the HTTP response cache, by the
* network, or by both in the event of a conditional GET.
*/
-public class HttpEngine {
+public final class HttpEngine {
+ /**
+ * How many redirects and auth challenges should we attempt? Chrome follows 21 redirects; Firefox,
+ * curl, and wget follow 20; Safari follows 16; and HTTP/1.0 recommends 5.
+ */
+ public static final int MAX_FOLLOW_UPS = 20;
+
+ private static final ResponseBody EMPTY_BODY = new ResponseBody() {
+ @Override public MediaType contentType() {
+ return null;
+ }
+ @Override public long contentLength() {
+ return 0;
+ }
+ @Override public BufferedSource source() {
+ return new Buffer();
+ }
+ };
+
final OkHttpClient client;
private Connection connection;
+ private Address address;
private RouteSelector routeSelector;
private Route route;
private final Response priorResponse;
@@ -119,12 +159,6 @@
private Response cacheResponse;
/**
- * The response read from the network. Null if the network response hasn't
- * been read yet, or if the network is not used. Never modified by OkHttp.
- */
- private Response networkResponse;
-
- /**
* The user-visible response. This is derived from either the network
* response, cache response, or both. It is customized to support OkHttp
* features like compression and caching.
@@ -133,41 +167,39 @@
private Sink requestBodyOut;
private BufferedSink bufferedRequestBody;
-
- private ResponseSource responseSource;
-
- /** Null until a response is received from the network or the cache. */
- private Source responseTransferSource;
- private Source responseBody;
- private InputStream responseBodyBytes;
+ private final boolean callerWritesRequestBody;
+ private final boolean forWebSocket;
/** The cache request currently being populated from a network response. */
private CacheRequest storeRequest;
+ private CacheStrategy cacheStrategy;
/**
- * @param request the HTTP request without a body. The body must be
- * written via the engine's request body stream.
- * @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 attempt, or null if this request doesn't
- * recover from a failure.
+ * @param request the HTTP request without a body. The body must be written via the engine's
+ * request body stream.
+ * @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,
- Connection connection, RouteSelector routeSelector, RetryableSink requestBodyOut,
- Response priorResponse) {
+ boolean callerWritesRequestBody, boolean forWebSocket, Connection connection,
+ RouteSelector routeSelector, 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.requestBodyOut = requestBodyOut;
this.priorResponse = priorResponse;
if (connection != null) {
- connection.setOwner(this);
+ Internal.instance.setOwner(connection, this);
this.route = connection.getRoute();
} else {
this.route = null;
@@ -179,64 +211,93 @@
* source if necessary. Prepares the request headers and gets ready to start
* writing the request body if it exists.
*/
- public final void sendRequest() throws IOException {
- if (responseSource != null) return; // Already sent.
+ public void sendRequest() throws IOException {
+ if (cacheStrategy != null) return; // Already sent.
if (transport != null) throw new IllegalStateException();
Request request = networkRequest(userRequest);
- OkResponseCache responseCache = client.getOkResponseCache();
+ InternalCache responseCache = Internal.instance.internalCache(client);
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
+
long now = System.currentTimeMillis();
- CacheStrategy cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
- responseSource = cacheStrategy.source;
+ cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
if (responseCache != null) {
- responseCache.trackResponse(responseSource);
+ responseCache.trackResponse(cacheStrategy);
}
- if (cacheCandidate != null
- && (responseSource == ResponseSource.NONE || cacheResponse == null)) {
+ if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
if (networkRequest != null) {
// Open a connection unless we inherited one from a redirect.
if (connection == null) {
- connect(networkRequest);
+ connect();
}
- // Blow up if we aren't the current owner of the connection.
- if (connection.getOwner() != this && !connection.isSpdy()) throw new AssertionError();
+ transport = Internal.instance.newTransport(connection, this);
- transport = (Transport) connection.newTransport(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) {
+ long contentLength = OkHeaders.contentLength(request);
+ if (bufferRequestBody) {
+ if (contentLength > Integer.MAX_VALUE) {
+ throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
+ + "setChunkedStreamingMode() for requests larger than 2 GiB.");
+ }
- // Create a request body if we don't have one already. We'll already have
- // one if we're retrying a failed POST.
- if (hasRequestBody() && requestBodyOut == null) {
- requestBodyOut = transport.createRequestBody(request);
+ if (contentLength != -1) {
+ // Buffer a request body of a known length.
+ transport.writeRequestHeaders(networkRequest);
+ requestBodyOut = new RetryableSink((int) contentLength);
+ } else {
+ // Buffer a request body of an unknown length. Don't write request
+ // headers until the entire body is ready; otherwise we can't set the
+ // Content-Length header correctly.
+ requestBodyOut = new RetryableSink();
+ }
+ } else {
+ transport.writeRequestHeaders(networkRequest);
+ requestBodyOut = transport.createRequestBody(networkRequest, contentLength);
+ }
}
} else {
- // We're using a cached response. Recycle a connection we may have inherited from a redirect.
+ // We aren't using the network. Recycle a connection we may have inherited from a redirect.
if (connection != null) {
- client.getConnectionPool().recycle(connection);
+ Internal.instance.recycle(client.getConnectionPool(), connection);
connection = null;
}
- // No need for the network! Promote the cached response immediately.
- this.userResponse = cacheResponse.newBuilder()
- .request(userRequest)
- .priorResponse(stripBody(priorResponse))
- .cacheResponse(stripBody(cacheResponse))
- .build();
- if (userResponse.body() != null) {
- initContentStream(userResponse.body().source());
+ if (cacheResponse != null) {
+ // We have a valid cached response. Promote it to the user response immediately.
+ this.userResponse = cacheResponse.newBuilder()
+ .request(userRequest)
+ .priorResponse(stripBody(priorResponse))
+ .cacheResponse(stripBody(cacheResponse))
+ .build();
+ } else {
+ // We're forbidden from using the network, and the cache is insufficient.
+ this.userResponse = new Response.Builder()
+ .request(userRequest)
+ .priorResponse(stripBody(priorResponse))
+ .protocol(Protocol.HTTP_1_1)
+ .code(504)
+ .message("Unsatisfiable Request (only-if-cached)")
+ .body(EMPTY_BODY)
+ .build();
}
+
+ userResponse = unzip(userResponse);
}
}
@@ -247,42 +308,44 @@
}
/** Connect to the origin server either directly or via a proxy. */
- private void connect(Request request) throws IOException {
+ private void connect() throws IOException {
if (connection != null) throw new IllegalStateException();
if (routeSelector == null) {
- String uriHost = request.url().getHost();
- if (uriHost == null || uriHost.length() == 0) {
- throw new UnknownHostException(request.url().toString());
- }
- SSLSocketFactory sslSocketFactory = null;
- HostnameVerifier hostnameVerifier = null;
- if (request.isHttps()) {
- sslSocketFactory = client.getSslSocketFactory();
- hostnameVerifier = client.getHostnameVerifier();
- }
- Address address = new Address(uriHost, getEffectivePort(request.url()),
- client.getSocketFactory(), sslSocketFactory, hostnameVerifier, client.getAuthenticator(),
- client.getProxy(), client.getProtocols());
- routeSelector = new RouteSelector(address, request.uri(), client.getProxySelector(),
- client.getConnectionPool(), client.getHostResolver(), client.getRoutesDatabase());
+ address = createAddress(client, networkRequest);
+ routeSelector = RouteSelector.get(address, networkRequest, client);
}
- connection = routeSelector.next(request.method());
- connection.setOwner(this);
-
- if (!connection.isConnected()) {
- connection.connect(client.getConnectTimeout(), client.getReadTimeout(), getTunnelConfig());
- if (connection.isSpdy()) client.getConnectionPool().share(connection);
- client.getRoutesDatabase().connected(connection.getRoute());
- } else if (!connection.isSpdy()) {
- connection.updateReadTimeout(client.getReadTimeout());
- }
-
+ connection = nextConnection();
route = connection.getRoute();
}
/**
+ * Returns the next connection to attempt.
+ *
+ * @throws java.util.NoSuchElementException if there are no more routes to attempt.
+ */
+ private Connection nextConnection() throws IOException {
+ Connection connection = createNextConnection();
+ Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
+ return connection;
+ }
+
+ private Connection createNextConnection() throws IOException {
+ 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;
+ }
+ pooled.getSocket().close();
+ }
+ Route route = routeSelector.next();
+ return new Connection(pool, route);
+ }
+
+ /**
* Called immediately before the transport transmits HTTP request headers.
* This is used to observe the sent time should the request be cached.
*/
@@ -291,17 +354,17 @@
sentRequestMillis = System.currentTimeMillis();
}
- boolean hasRequestBody() {
- return HttpMethod.hasRequestBody(userRequest.method());
+ boolean permitsRequestBody() {
+ return HttpMethod.permitsRequestBody(userRequest.method());
}
/** Returns the request body or null if this request doesn't have a body. */
- public final Sink getRequestBody() {
- if (responseSource == null) throw new IllegalStateException();
+ public Sink getRequestBody() {
+ if (cacheStrategy == null) throw new IllegalStateException();
return requestBodyOut;
}
- public final BufferedSink getBufferedRequestBody() {
+ public BufferedSink getBufferedRequestBody() {
BufferedSink result = bufferedRequestBody;
if (result != null) return result;
Sink requestBody = getRequestBody();
@@ -310,45 +373,34 @@
: null;
}
- public final boolean hasResponse() {
+ public boolean hasResponse() {
return userResponse != null;
}
- public final Request getRequest() {
+ public Request getRequest() {
return userRequest;
}
/** Returns the engine's response. */
// TODO: the returned body will always be null.
- public final Response getResponse() {
+ public Response getResponse() {
if (userResponse == null) throw new IllegalStateException();
return userResponse;
}
- public final Source getResponseBody() {
- if (userResponse == null) throw new IllegalStateException();
- return responseBody;
- }
-
- public final InputStream getResponseBodyBytes() {
- InputStream result = responseBodyBytes;
- return result != null
- ? result
- : (responseBodyBytes = Okio.buffer(getResponseBody()).inputStream());
- }
-
- public final Connection getConnection() {
+ public Connection getConnection() {
return connection;
}
/**
* Report and attempt to recover from {@code e}. Returns a new HTTP engine
* that should be used for the retry if {@code e} is recoverable, or null if
- * the failure is permanent.
+ * the failure is permanent. Requests with a body can only be recovered if the
+ * body is buffered.
*/
- public HttpEngine recover(IOException e) {
+ public HttpEngine recover(IOException e, Sink requestBodyOut) {
if (routeSelector != null && connection != null) {
- routeSelector.connectFailed(connection, e);
+ connectFailed(routeSelector, e);
}
boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
@@ -362,17 +414,45 @@
Connection connection = close();
// For failure recovery, use the same route selector with a new connection.
- return new HttpEngine(client, userRequest, bufferRequestBody, connection, routeSelector,
- (RetryableSink) requestBodyOut, priorResponse);
+ 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);
+ }
+
+ 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 the problem was a CertificateException from the X509TrustManager,
// do not retry, we didn't have an abrupt server-initiated exception.
- boolean sslFailure =
- e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException;
- boolean protocolFailure = e instanceof ProtocolException;
- return !sslFailure && !protocolFailure;
+ if (e instanceof SSLPeerUnverifiedException
+ || (e instanceof SSLHandshakeException && e.getCause() instanceof CertificateException)) {
+ 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;
}
/**
@@ -384,12 +464,18 @@
}
private void maybeCache() throws IOException {
- OkResponseCache responseCache = client.getOkResponseCache();
+ InternalCache responseCache = Internal.instance.internalCache(client);
if (responseCache == null) return;
// Should we cache this response for this request?
if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
- responseCache.maybeRemove(networkRequest);
+ if (HttpMethod.invalidatesCache(networkRequest.method())) {
+ try {
+ responseCache.remove(networkRequest);
+ } catch (IOException ignored) {
+ // The cache cannot be written.
+ }
+ }
return;
}
@@ -402,7 +488,7 @@
* either exhausted or closed. If it is unneeded when this is called, it will
* be released immediately.
*/
- public final void releaseConnection() throws IOException {
+ public void releaseConnection() throws IOException {
if (transport != null && connection != null) {
transport.releaseConnectionOnIdle();
}
@@ -415,9 +501,12 @@
* the caller's responsibility to close the request body and response body
* streams; otherwise resources may be leaked.
*/
- public final void disconnect() throws IOException {
+ public void disconnect() {
if (transport != null) {
- transport.disconnect(this);
+ try {
+ transport.disconnect(this);
+ } catch (IOException ignored) {
+ }
}
}
@@ -425,7 +514,7 @@
* Release any resources held by this engine. If a connection is still held by
* this engine, it is returned.
*/
- public final Connection close() {
+ public Connection close() {
if (bufferedRequestBody != null) {
// This also closes the wrapped requestBodyOut.
closeQuietly(bufferedRequestBody);
@@ -434,27 +523,24 @@
}
// If this engine never achieved a response body, its connection cannot be reused.
- if (responseBody == null) {
- closeQuietly(connection);
+ if (userResponse == null) {
+ if (connection != null) closeQuietly(connection.getSocket()); // TODO: does this break SPDY?
connection = null;
return null;
}
// Close the response body. This will recycle the connection if it is eligible.
- closeQuietly(responseBody);
-
- // Clear the buffer held by the response body input stream adapter.
- closeQuietly(responseBodyBytes);
+ closeQuietly(userResponse.body());
// Close the connection if it cannot be reused.
- if (transport != null && !transport.canReuseConnection()) {
- closeQuietly(connection);
+ 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 && !connection.clearOwner()) {
+ if (connection != null && !Internal.instance.clearOwner(connection)) {
connection = null;
}
@@ -464,45 +550,49 @@
}
/**
- * Initialize the response content stream from the response transfer source.
- * These two sources are the same unless we're doing transparent gzip, in
- * which case the content source is decompressed.
+ * Returns a new response that does gzip decompression on {@code response}, if transparent gzip
+ * was both offered by OkHttp and used by the origin server.
*
- * <p>Whenever we do transparent gzip we also strip the corresponding headers.
- * We strip the Content-Encoding header to prevent the application from
- * attempting to double decompress. We strip the Content-Length header because
- * it is the length of the compressed content, but the application is only
- * interested in the length of the uncompressed content.
+ * <p>In addition to decompression, this will also strip the corresponding headers. We strip the
+ * Content-Encoding header to prevent the application from attempting to double decompress. We
+ * strip the Content-Length header because it is the length of the compressed content, but the
+ * application is only interested in the length of the uncompressed content.
*
- * <p>This method should only be used for non-empty response bodies. Response
- * codes like "304 Not Modified" can include "Content-Encoding: gzip" without
- * a response body and we will crash if we attempt to decompress the zero-byte
- * source.
+ * <p>This method should only be used for non-empty response bodies. Response codes like "304 Not
+ * Modified" can include "Content-Encoding: gzip" without a response body and we will crash if we
+ * attempt to decompress the zero-byte source.
*/
- private void initContentStream(Source transferSource) throws IOException {
- responseTransferSource = transferSource;
- if (transparentGzip && "gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) {
- userResponse = userResponse.newBuilder()
- .removeHeader("Content-Encoding")
- .removeHeader("Content-Length")
- .build();
- responseBody = new GzipSource(transferSource);
- } else {
- responseBody = transferSource;
+ private Response unzip(final Response response) throws IOException {
+ if (!transparentGzip || !"gzip".equalsIgnoreCase(userResponse.header("Content-Encoding"))) {
+ return response;
}
+
+ if (response.body() == null) {
+ return response;
+ }
+
+ GzipSource responseBody = new GzipSource(response.body().source());
+ Headers strippedHeaders = response.headers().newBuilder()
+ .removeAll("Content-Encoding")
+ .removeAll("Content-Length")
+ .build();
+ return response.newBuilder()
+ .headers(strippedHeaders)
+ .body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)))
+ .build();
}
/**
* Returns true if the response must have a (possibly 0-length) body.
* See RFC 2616 section 4.3.
*/
- public final boolean hasResponseBody() {
+ public static boolean hasBody(Response response) {
// HEAD requests never yield a body regardless of the response headers.
- if (userRequest.method().equals("HEAD")) {
+ if (response.request().method().equals("HEAD")) {
return false;
}
- int responseCode = userResponse.code();
+ int responseCode = response.code();
if ((responseCode < HTTP_CONTINUE || responseCode >= 200)
&& responseCode != HTTP_NO_CONTENT
&& responseCode != HTTP_NOT_MODIFIED) {
@@ -512,8 +602,8 @@
// If the Content-Length or Transfer-Encoding headers disagree with the
// response code, the response is malformed. For best compatibility, we
// honor the headers.
- if (OkHeaders.contentLength(networkResponse) != -1
- || "chunked".equalsIgnoreCase(networkResponse.header("Transfer-Encoding"))) {
+ if (OkHeaders.contentLength(response) != -1
+ || "chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
return true;
}
@@ -529,15 +619,11 @@
private Request networkRequest(Request request) throws IOException {
Request.Builder result = request.newBuilder();
- if (request.getUserAgent() == null) {
- result.setUserAgent(getDefaultUserAgent());
- }
-
if (request.header("Host") == null) {
result.header("Host", hostHeader(request.url()));
}
- if ((connection == null || connection.getHttpMinorVersion() != 0)
+ if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0)
&& request.header("Connection") == null) {
result.header("Connection", "Keep-Alive");
}
@@ -547,10 +633,6 @@
result.header("Accept-Encoding", "gzip");
}
- if (hasRequestBody() && request.header("Content-Type") == null) {
- result.header("Content-Type", "application/x-www-form-urlencoded");
- }
-
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
// Capture the request headers added so far so that they can be offered to the CookieHandler.
@@ -564,12 +646,11 @@
OkHeaders.addCookies(result, cookies);
}
- return result.build();
- }
+ if (request.header("User-Agent") == null) {
+ result.header("User-Agent", Version.userAgent());
+ }
- public static String getDefaultUserAgent() {
- String agent = System.getProperty("http.agent");
- return agent != null ? agent : ("Java" + System.getProperty("java.version"));
+ return result.build();
}
public static String hostHeader(URL url) {
@@ -582,7 +663,7 @@
* Flushes the remaining request header and body, parses the HTTP response
* headers and starts reading the HTTP response body if it exists.
*/
- public final void readResponse() throws IOException {
+ public void readResponse() throws IOException {
if (userResponse != null) {
return; // Already ready.
}
@@ -593,49 +674,54 @@
return; // No network response to read.
}
- // Flush the request body if there's data outstanding.
- if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) {
- bufferedRequestBody.flush();
- }
+ Response networkResponse;
- if (sentRequestMillis == -1) {
- if (OkHeaders.contentLength(networkRequest) == -1
- && requestBodyOut instanceof RetryableSink) {
- // We might not learn the Content-Length until the request body has been buffered.
- long contentLength = ((RetryableSink) requestBodyOut).contentLength();
- networkRequest = networkRequest.newBuilder()
- .header("Content-Length", Long.toString(contentLength))
- .build();
- }
+ if (forWebSocket) {
transport.writeRequestHeaders(networkRequest);
+ networkResponse = readNetworkResponse();
+
+ } else if (!callerWritesRequestBody) {
+ networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest);
+
+ } else {
+ // Emit the request body's buffer so that everything is in requestBodyOut.
+ if (bufferedRequestBody != null && bufferedRequestBody.buffer().size() > 0) {
+ bufferedRequestBody.emit();
+ }
+
+ // Emit the request headers if we haven't yet. We might have just learned the Content-Length.
+ if (sentRequestMillis == -1) {
+ if (OkHeaders.contentLength(networkRequest) == -1
+ && requestBodyOut instanceof RetryableSink) {
+ long contentLength = ((RetryableSink) requestBodyOut).contentLength();
+ networkRequest = networkRequest.newBuilder()
+ .header("Content-Length", Long.toString(contentLength))
+ .build();
+ }
+ transport.writeRequestHeaders(networkRequest);
+ }
+
+ // Write the request body to the socket.
+ if (requestBodyOut != null) {
+ if (bufferedRequestBody != null) {
+ // This also closes the wrapped requestBodyOut.
+ bufferedRequestBody.close();
+ } else {
+ requestBodyOut.close();
+ }
+ if (requestBodyOut instanceof RetryableSink) {
+ transport.writeRequestBody((RetryableSink) requestBodyOut);
+ }
+ }
+
+ networkResponse = readNetworkResponse();
}
- if (requestBodyOut != null) {
- if (bufferedRequestBody != null) {
- // This also closes the wrapped requestBodyOut.
- bufferedRequestBody.close();
- } else {
- requestBodyOut.close();
- }
- if (requestBodyOut instanceof RetryableSink) {
- transport.writeRequestBody((RetryableSink) requestBodyOut);
- }
- }
-
- transport.flushRequest();
-
- networkResponse = transport.readResponseHeaders()
- .request(networkRequest)
- .handshake(connection.getHandshake())
- .header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
- .header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
- .setResponseSource(responseSource)
- .build();
- connection.setHttpMinorVersion(networkResponse.httpMinorVersion());
receiveHeaders(networkResponse.headers());
- if (responseSource == ResponseSource.CONDITIONAL_CACHE) {
- if (cacheResponse.validate(networkResponse)) {
+ // If we have a cache response too, then we're doing a conditional get.
+ if (cacheResponse != null) {
+ if (validate(cacheResponse, networkResponse)) {
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
@@ -643,18 +729,15 @@
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
- transport.emptyTransferStream();
+ networkResponse.body().close();
releaseConnection();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
- OkResponseCache responseCache = client.getOkResponseCache();
+ InternalCache responseCache = Internal.instance.internalCache(client);
responseCache.trackConditionalCacheHit();
responseCache.update(cacheResponse, stripBody(userResponse));
- if (cacheResponse.body() != null) {
- initContentStream(cacheResponse.body().source());
- }
-
+ userResponse = unzip(userResponse);
return;
} else {
closeQuietly(cacheResponse.body());
@@ -668,15 +751,183 @@
.networkResponse(stripBody(networkResponse))
.build();
- if (!hasResponseBody()) {
- // Don't call initContentStream() when the response doesn't have any content.
- responseTransferSource = transport.getTransferStream(storeRequest);
- responseBody = responseTransferSource;
- return;
+ if (hasBody(userResponse)) {
+ maybeCache();
+ userResponse = unzip(cacheWritingResponse(storeRequest, userResponse));
+ }
+ }
+
+ class NetworkInterceptorChain implements Interceptor.Chain {
+ private final int index;
+ private final Request request;
+ private int calls;
+
+ NetworkInterceptorChain(int index, Request request) {
+ this.index = index;
+ this.request = request;
}
- maybeCache();
- initContentStream(transport.getTransferStream(storeRequest));
+ @Override public Connection connection() {
+ return connection;
+ }
+
+ @Override public Request request() {
+ return request;
+ }
+
+ @Override public Response proceed(Request request) throws IOException {
+ calls++;
+
+ if (index > 0) {
+ Interceptor caller = client.networkInterceptors().get(index - 1);
+ Address address = connection().getRoute().getAddress();
+
+ // Confirm that the interceptor uses the connection we've already prepared.
+ if (!request.url().getHost().equals(address.getUriHost())
+ || getEffectivePort(request.url()) != address.getUriPort()) {
+ throw new IllegalStateException("network interceptor " + caller
+ + " must retain the same host and port");
+ }
+
+ // Confirm that this is the interceptor's first call to chain.proceed().
+ if (calls > 1) {
+ throw new IllegalStateException("network interceptor " + caller
+ + " must call proceed() exactly once");
+ }
+ }
+
+ if (index < client.networkInterceptors().size()) {
+ // There's another interceptor in the chain. Call that.
+ NetworkInterceptorChain chain = new NetworkInterceptorChain(index + 1, request);
+ Interceptor interceptor = client.networkInterceptors().get(index);
+ Response interceptedResponse = interceptor.intercept(chain);
+
+ // Confirm that the interceptor made the required call to chain.proceed().
+ if (chain.calls != 1) {
+ throw new IllegalStateException("network interceptor " + interceptor
+ + " must call proceed() exactly once");
+ }
+
+ return interceptedResponse;
+ }
+
+ transport.writeRequestHeaders(request);
+
+ if (permitsRequestBody() && request.body() != null) {
+ Sink requestBodyOut = transport.createRequestBody(request, request.body().contentLength());
+ BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
+ request.body().writeTo(bufferedRequestBody);
+ bufferedRequestBody.close();
+ }
+
+ return readNetworkResponse();
+ }
+ }
+
+ private Response readNetworkResponse() throws IOException {
+ transport.finishRequest();
+
+ Response networkResponse = transport.readResponseHeaders()
+ .request(networkRequest)
+ .handshake(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))
+ .build();
+ }
+
+ Internal.instance.setProtocol(connection, networkResponse.protocol());
+ return networkResponse;
+ }
+
+ /**
+ * Returns a new source that writes bytes to {@code cacheRequest} as they are read by the source
+ * consumer. This is careful to discard bytes left over when the stream is closed; otherwise we
+ * may never exhaust the source stream and therefore not complete the cached response.
+ */
+ private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)
+ throws IOException {
+ // Some apps return a null body; for compatibility we treat that like a null cache request.
+ if (cacheRequest == null) return response;
+ Sink cacheBodyUnbuffered = cacheRequest.body();
+ if (cacheBodyUnbuffered == null) return response;
+
+ final BufferedSource source = response.body().source();
+ final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);
+
+ Source cacheWritingSource = new Source() {
+ boolean cacheRequestClosed;
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ long bytesRead;
+ try {
+ bytesRead = source.read(sink, byteCount);
+ } catch (IOException e) {
+ if (!cacheRequestClosed) {
+ cacheRequestClosed = true;
+ cacheRequest.abort(); // Failed to write a complete cache response.
+ }
+ throw e;
+ }
+
+ if (bytesRead == -1) {
+ if (!cacheRequestClosed) {
+ cacheRequestClosed = true;
+ cacheBody.close(); // The cache response is complete!
+ }
+ return -1;
+ }
+
+ sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);
+ cacheBody.emitCompleteSegments();
+ return bytesRead;
+ }
+
+ @Override public Timeout timeout() {
+ return source.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ if (!cacheRequestClosed
+ && !Util.discard(this, Transport.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
+ cacheRequestClosed = true;
+ cacheRequest.abort();
+ }
+ source.close();
+ }
+ };
+
+ return response.newBuilder()
+ .body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource)))
+ .build();
+ }
+
+ /**
+ * Returns true if {@code cached} should be used; false if {@code network}
+ * response should be used.
+ */
+ private static boolean validate(Response cached, Response network) {
+ if (network.code() == HTTP_NOT_MODIFIED) {
+ return true;
+ }
+
+ // The HTTP spec says that if the network's response is older than our
+ // cached response, we may return the cache's response. Like Chrome (but
+ // unlike Firefox), this client prefers to return the newer response.
+ Date lastModified = cached.headers().getDate("Last-Modified");
+ if (lastModified != null) {
+ Date networkLastModified = network.headers().getDate("Last-Modified");
+ if (networkLastModified != null
+ && networkLastModified.getTime() < lastModified.getTime()) {
+ return true;
+ }
+ }
+
+ return false;
}
/**
@@ -686,20 +937,23 @@
private static Headers combine(Headers cachedHeaders, Headers networkHeaders) throws IOException {
Headers.Builder result = new Headers.Builder();
- for (int i = 0; i < cachedHeaders.size(); i++) {
+ for (int i = 0, size = cachedHeaders.size(); i < size; i++) {
String fieldName = cachedHeaders.name(i);
String value = cachedHeaders.value(i);
- if ("Warning".equals(fieldName) && value.startsWith("1")) {
- continue; // drop 100-level freshness warnings
+ if ("Warning".equalsIgnoreCase(fieldName) && value.startsWith("1")) {
+ continue; // Drop 100-level freshness warnings.
}
- if (!isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) {
+ if (!OkHeaders.isEndToEnd(fieldName) || networkHeaders.get(fieldName) == null) {
result.add(fieldName, value);
}
}
- for (int i = 0; i < networkHeaders.size(); i++) {
+ for (int i = 0, size = networkHeaders.size(); i < size; i++) {
String fieldName = networkHeaders.name(i);
- if (isEndToEnd(fieldName)) {
+ if ("Content-Length".equalsIgnoreCase(fieldName)) {
+ continue; // Ignore content-length headers of validating responses.
+ }
+ if (OkHeaders.isEndToEnd(fieldName)) {
result.add(fieldName, networkHeaders.value(i));
}
}
@@ -707,36 +961,113 @@
return result.build();
}
- /**
- * Returns true if {@code fieldName} is an end-to-end HTTP header, as
- * defined by RFC 2616, 13.5.1.
- */
- private static boolean isEndToEnd(String fieldName) {
- return !"Connection".equalsIgnoreCase(fieldName)
- && !"Keep-Alive".equalsIgnoreCase(fieldName)
- && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
- && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
- && !"TE".equalsIgnoreCase(fieldName)
- && !"Trailers".equalsIgnoreCase(fieldName)
- && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
- && !"Upgrade".equalsIgnoreCase(fieldName);
- }
-
- private TunnelRequest getTunnelConfig() {
- if (!userRequest.isHttps()) return null;
-
- String userAgent = userRequest.getUserAgent();
- if (userAgent == null) userAgent = getDefaultUserAgent();
-
- URL url = userRequest.url();
- return new TunnelRequest(url.getHost(), getEffectivePort(url), userAgent,
- userRequest.getProxyAuthorization());
- }
-
public void receiveHeaders(Headers headers) throws IOException {
CookieHandler cookieHandler = client.getCookieHandler();
if (cookieHandler != null) {
cookieHandler.put(userRequest.uri(), OkHeaders.toMultimap(headers, null));
}
}
+
+ /**
+ * Figures out the HTTP request to make in response to receiving this engine's
+ * response. This will either add authentication headers or follow redirects.
+ * If a follow-up is either unnecessary or not applicable, this returns null.
+ */
+ public Request followUpRequest() throws IOException {
+ if (userResponse == null) throw new IllegalStateException();
+ Proxy selectedProxy = getRoute() != null
+ ? getRoute().getProxy()
+ : client.getProxy();
+ int responseCode = userResponse.code();
+
+ switch (responseCode) {
+ case HTTP_PROXY_AUTH:
+ if (selectedProxy.type() != Proxy.Type.HTTP) {
+ throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
+ }
+ // fall-through
+ case HTTP_UNAUTHORIZED:
+ return OkHeaders.processAuthHeader(client.getAuthenticator(), userResponse, selectedProxy);
+
+ case HTTP_PERM_REDIRECT:
+ 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")) {
+ return null;
+ }
+ // fall-through
+ case HTTP_MULT_CHOICE:
+ case HTTP_MOVED_PERM:
+ case HTTP_MOVED_TEMP:
+ case HTTP_SEE_OTHER:
+ // Does the client allow redirects?
+ if (!client.getFollowRedirects()) return null;
+
+ String location = userResponse.header("Location");
+ if (location == null) return null;
+ URL url = new URL(userRequest.url(), location);
+
+ // Don't follow redirects to unsupported protocols.
+ if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) return null;
+
+ // If configured, don't follow redirects between SSL and non-SSL.
+ boolean sameProtocol = url.getProtocol().equals(userRequest.url().getProtocol());
+ if (!sameProtocol && !client.getFollowSslRedirects()) return null;
+
+ // Redirects don't include a request body.
+ Request.Builder requestBuilder = userRequest.newBuilder();
+ if (HttpMethod.permitsRequestBody(userRequest.method())) {
+ requestBuilder.method("GET", null);
+ requestBuilder.removeHeader("Transfer-Encoding");
+ requestBuilder.removeHeader("Content-Length");
+ requestBuilder.removeHeader("Content-Type");
+ }
+
+ // When redirecting across hosts, drop all authentication headers. This
+ // is potentially annoying to the application layer since they have no
+ // way to retain them.
+ if (!sameConnection(url)) {
+ requestBuilder.removeHeader("Authorization");
+ }
+
+ return requestBuilder.url(url).build();
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Returns true if an HTTP request for {@code followUp} can reuse the
+ * connection used by this engine.
+ */
+ public boolean sameConnection(URL followUp) {
+ URL url = userRequest.url();
+ return url.getHost().equals(followUp.getHost())
+ && getEffectivePort(url) == getEffectivePort(followUp)
+ && url.getProtocol().equals(followUp.getProtocol());
+ }
+
+ private static Address createAddress(OkHttpClient client, Request request)
+ throws UnknownHostException {
+ String uriHost = request.url().getHost();
+ if (uriHost == null || uriHost.length() == 0) {
+ throw new UnknownHostException(request.url().toString());
+ }
+
+ SSLSocketFactory sslSocketFactory = null;
+ HostnameVerifier hostnameVerifier = null;
+ CertificatePinner certificatePinner = null;
+ if (request.isHttps()) {
+ sslSocketFactory = client.getSslSocketFactory();
+ hostnameVerifier = client.getHostnameVerifier();
+ certificatePinner = client.getCertificatePinner();
+ }
+
+ return new Address(uriHost, getEffectivePort(request.url()),
+ 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 1577d10..b5f2a48 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
@@ -15,14 +15,7 @@
*/
package com.squareup.okhttp.internal.http;
-import java.util.Arrays;
-import java.util.LinkedHashSet;
-import java.util.Set;
-
public final class HttpMethod {
- public static final Set<String> METHODS = new LinkedHashSet<String>(Arrays.asList(
- "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "PATCH"));
-
public static boolean invalidatesCache(String method) {
return method.equals("POST")
|| method.equals("PATCH")
@@ -30,10 +23,14 @@
|| method.equals("DELETE");
}
- public static boolean hasRequestBody(String method) {
+ public static boolean requiresRequestBody(String method) {
return method.equals("POST")
|| method.equals("PUT")
- || method.equals("PATCH")
+ || method.equals("PATCH");
+ }
+
+ public static boolean permitsRequestBody(String method) {
+ return requiresRequestBody(method)
|| method.equals("DELETE"); // Permitted as spec is ambiguous.
}
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
index 2ffe039..d02e1e5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
@@ -18,8 +18,9 @@
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
-import java.net.CacheRequest;
+import okio.Okio;
import okio.Sink;
import okio.Source;
@@ -32,36 +33,14 @@
this.httpConnection = httpConnection;
}
- @Override public Sink createRequestBody(Request request) throws IOException {
- long contentLength = OkHeaders.contentLength(request);
-
- if (httpEngine.bufferRequestBody) {
- if (contentLength > Integer.MAX_VALUE) {
- throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
- + "setChunkedStreamingMode() for requests larger than 2 GiB.");
- }
-
- if (contentLength != -1) {
- // Buffer a request body of a known length.
- writeRequestHeaders(request);
- return new RetryableSink((int) contentLength);
- } else {
- // Buffer a request body of an unknown length. Don't write request
- // headers until the entire body is ready; otherwise we can't set the
- // Content-Length header correctly.
- return new RetryableSink();
- }
- }
-
+ @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
// Stream a request body of unknown length.
- writeRequestHeaders(request);
return httpConnection.newChunkedSink();
}
if (contentLength != -1) {
// Stream a request body of a known length.
- writeRequestHeaders(request);
return httpConnection.newFixedLengthSink(contentLength);
}
@@ -69,7 +48,7 @@
"Cannot stream a request body without chunked encoding or a known content length!");
}
- @Override public void flushRequest() throws IOException {
+ @Override public void finishRequest() throws IOException {
httpConnection.flush();
}
@@ -93,8 +72,8 @@
httpEngine.writingRequestHeaders();
String requestLine = RequestLine.get(request,
httpEngine.getConnection().getRoute().getProxy().type(),
- httpEngine.getConnection().getHttpMinorVersion());
- httpConnection.writeRequest(request.getHeaders(), requestLine);
+ httpEngine.getConnection().getProtocol());
+ httpConnection.writeRequest(request.headers(), requestLine);
}
@Override public Response.Builder readResponseHeaders() throws IOException {
@@ -127,28 +106,29 @@
return true;
}
- @Override public void emptyTransferStream() throws IOException {
- httpConnection.emptyResponseBody();
+ @Override public ResponseBody openResponseBody(Response response) throws IOException {
+ Source source = getTransferStream(response);
+ return new RealResponseBody(response.headers(), Okio.buffer(source));
}
- @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException {
- if (!httpEngine.hasResponseBody()) {
- return httpConnection.newFixedLengthSource(cacheRequest, 0);
+ private Source getTransferStream(Response response) throws IOException {
+ if (!HttpEngine.hasBody(response)) {
+ return httpConnection.newFixedLengthSource(0);
}
- if ("chunked".equalsIgnoreCase(httpEngine.getResponse().header("Transfer-Encoding"))) {
- return httpConnection.newChunkedSource(cacheRequest, httpEngine);
+ if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
+ return httpConnection.newChunkedSource(httpEngine);
}
- long contentLength = OkHeaders.contentLength(httpEngine.getResponse());
+ long contentLength = OkHeaders.contentLength(response);
if (contentLength != -1) {
- return httpConnection.newFixedLengthSource(cacheRequest, contentLength);
+ 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(cacheRequest);
+ return httpConnection.newUnknownLengthSource();
}
@Override public void disconnect(HttpEngine engine) throws IOException {
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 456c9c7..a39c657 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
@@ -1,15 +1,24 @@
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.Authenticator;
+import com.squareup.okhttp.Challenge;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.Platform;
+import java.io.IOException;
+import java.net.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.TreeMap;
+import java.util.TreeSet;
+
+import static com.squareup.okhttp.internal.Util.equal;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
/** Headers and utilities for internal use by OkHttp. */
public final class OkHeaders {
@@ -41,12 +50,6 @@
public static final String RECEIVED_MILLIS = PREFIX + "-Received-Millis";
/**
- * Synthetic response header: the response source and status code like
- * "CONDITIONAL_CACHE 304".
- */
- public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
-
- /**
* Synthetic response header: the selected
* {@link com.squareup.okhttp.Protocol protocol} ("spdy/3.1", "http/1.1", etc).
*/
@@ -83,12 +86,12 @@
* for responses. If non-null, this value is mapped to the null key.
*/
public static Map<String, List<String>> toMultimap(Headers headers, String valueForNullKey) {
- Map<String, List<String>> result = new TreeMap<String, List<String>>(FIELD_NAME_COMPARATOR);
- for (int i = 0; i < headers.size(); i++) {
+ Map<String, List<String>> result = new TreeMap<>(FIELD_NAME_COMPARATOR);
+ for (int i = 0, size = headers.size(); i < size; i++) {
String fieldName = headers.name(i);
String value = headers.value(i);
- List<String> allValues = new ArrayList<String>();
+ List<String> allValues = new ArrayList<>();
List<String> otherValues = result.get(fieldName);
if (otherValues != null) {
allValues.addAll(otherValues);
@@ -119,10 +122,143 @@
private static String buildCookieHeader(List<String> cookies) {
if (cookies.size() == 1) return cookies.get(0);
StringBuilder sb = new StringBuilder();
- for (int i = 0; i < cookies.size(); i++) {
+ for (int i = 0, size = cookies.size(); i < size; i++) {
if (i > 0) sb.append("; ");
sb.append(cookies.get(i));
}
return sb.toString();
}
+
+ /**
+ * Returns true if none of the Vary headers have changed between {@code
+ * cachedRequest} and {@code newRequest}.
+ */
+ public static boolean varyMatches(
+ Response cachedResponse, Headers cachedRequest, Request newRequest) {
+ for (String field : varyFields(cachedResponse)) {
+ if (!equal(cachedRequest.values(field), newRequest.headers(field))) return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns true if a Vary header contains an asterisk. Such responses cannot
+ * be cached.
+ */
+ public static boolean hasVaryAll(Response response) {
+ return varyFields(response).contains("*");
+ }
+
+ private static Set<String> varyFields(Response response) {
+ Set<String> result = Collections.emptySet();
+ Headers headers = response.headers();
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ if (!"Vary".equalsIgnoreCase(headers.name(i))) continue;
+
+ String value = headers.value(i);
+ if (result.isEmpty()) {
+ result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
+ }
+ for (String varyField : value.split(",")) {
+ result.add(varyField.trim());
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Returns the subset of the headers in {@code response}'s request that
+ * impact the content of response's body.
+ */
+ public static Headers varyHeaders(Response response) {
+ Set<String> varyFields = varyFields(response);
+ if (varyFields.isEmpty()) return new Headers.Builder().build();
+
+ // Use the request headers sent over the network, since that's what the
+ // response varies on. Otherwise OkHttp-supplied headers like
+ // "Accept-Encoding: gzip" may be lost.
+ Headers requestHeaders = response.networkResponse().request().headers();
+
+ Headers.Builder result = new Headers.Builder();
+ for (int i = 0, size = requestHeaders.size(); i < size; i++) {
+ String fieldName = requestHeaders.name(i);
+ if (varyFields.contains(fieldName)) {
+ result.add(fieldName, requestHeaders.value(i));
+ }
+ }
+ return result.build();
+ }
+
+ /**
+ * Returns true if {@code fieldName} is an end-to-end HTTP header, as
+ * defined by RFC 2616, 13.5.1.
+ */
+ static boolean isEndToEnd(String fieldName) {
+ return !"Connection".equalsIgnoreCase(fieldName)
+ && !"Keep-Alive".equalsIgnoreCase(fieldName)
+ && !"Proxy-Authenticate".equalsIgnoreCase(fieldName)
+ && !"Proxy-Authorization".equalsIgnoreCase(fieldName)
+ && !"TE".equalsIgnoreCase(fieldName)
+ && !"Trailers".equalsIgnoreCase(fieldName)
+ && !"Transfer-Encoding".equalsIgnoreCase(fieldName)
+ && !"Upgrade".equalsIgnoreCase(fieldName);
+ }
+
+ /**
+ * Parse RFC 2617 challenges. This API is only interested in the scheme
+ * name and realm.
+ */
+ public static List<Challenge> parseChallenges(Headers responseHeaders, String challengeHeader) {
+ // auth-scheme = token
+ // auth-param = token "=" ( token | quoted-string )
+ // challenge = auth-scheme 1*SP 1#auth-param
+ // realm = "realm" "=" realm-value
+ // realm-value = quoted-string
+ List<Challenge> result = new ArrayList<>();
+ for (int i = 0, size = responseHeaders.size(); i < size; i++) {
+ if (!challengeHeader.equalsIgnoreCase(responseHeaders.name(i))) {
+ continue;
+ }
+ String value = responseHeaders.value(i);
+ int pos = 0;
+ while (pos < value.length()) {
+ int tokenStart = pos;
+ pos = HeaderParser.skipUntil(value, pos, " ");
+
+ String scheme = value.substring(tokenStart, pos).trim();
+ pos = HeaderParser.skipWhitespace(value, pos);
+
+ // TODO: This currently only handles schemes with a 'realm' parameter;
+ // It needs to be fixed to handle any scheme and any parameters
+ // http://code.google.com/p/android/issues/detail?id=11140
+
+ if (!value.regionMatches(true, pos, "realm=\"", 0, "realm=\"".length())) {
+ break; // Unexpected challenge parameter; give up!
+ }
+
+ pos += "realm=\"".length();
+ int realmStart = pos;
+ pos = HeaderParser.skipUntil(value, pos, "\"");
+ String realm = value.substring(realmStart, pos);
+ pos++; // Consume '"' close quote.
+ pos = HeaderParser.skipUntil(value, pos, ",");
+ pos++; // Consume ',' comma.
+ pos = HeaderParser.skipWhitespace(value, pos);
+ result.add(new Challenge(scheme, realm));
+ }
+ }
+ return result;
+ }
+
+ /**
+ * React to a failed authorization response by looking up new credentials.
+ * Returns a request for a subsequent attempt, or null if no further attempts
+ * should be made.
+ */
+ public static Request processAuthHeader(Authenticator authenticator, Response response,
+ Proxy proxy) throws IOException {
+ return response.code() == HTTP_PROXY_AUTH
+ ? authenticator.authenticateProxy(proxy, response)
+ : authenticator.authenticate(proxy, response);
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RealResponseBody.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RealResponseBody.java
new file mode 100644
index 0000000..18d026f
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RealResponseBody.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2014 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.Headers;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.ResponseBody;
+import okio.BufferedSource;
+
+public final class RealResponseBody extends ResponseBody {
+ private final Headers headers;
+ private final BufferedSource source;
+
+ public RealResponseBody(Headers headers, BufferedSource source) {
+ this.headers = headers;
+ this.source = source;
+ }
+
+ @Override public MediaType contentType() {
+ String contentType = headers.get("Content-Type");
+ return contentType != null ? MediaType.parse(contentType) : null;
+ }
+
+ @Override public long contentLength() {
+ return OkHeaders.contentLength(headers);
+ }
+
+ @Override public BufferedSource source() {
+ return source;
+ }
+}
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 c918df3..f764afd 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,6 +1,8 @@
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
+import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
@@ -10,13 +12,13 @@
/**
* Returns the request status line, like "GET / HTTP/1.1". This is exposed
- * to the application by {@link HttpURLConnectionImpl#getHeaderFields}, so
- * it needs to be set even if the transport is SPDY.
+ * 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, int httpMinorVersion) {
+ static String get(Request request, Proxy.Type proxyType, Protocol protocol) {
StringBuilder result = new StringBuilder();
result.append(request.method());
- result.append(" ");
+ result.append(' ');
if (includeAuthorityInRequestLine(request, proxyType)) {
result.append(request.url());
@@ -24,8 +26,8 @@
result.append(requestPath(request.url()));
}
- result.append(" ");
- result.append(version(httpMinorVersion));
+ result.append(' ');
+ result.append(version(protocol));
return result.toString();
}
@@ -49,7 +51,7 @@
return pathAndQuery;
}
- public static String version(int httpMinorVersion) {
- return httpMinorVersion == 1 ? "HTTP/1.1" : "HTTP/1.0";
+ 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/RetryableSink.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableSink.java
index b8f53a3..371769f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableSink.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RetryableSink.java
@@ -18,10 +18,9 @@
import java.io.IOException;
import java.net.ProtocolException;
-import okio.BufferedSink;
-import okio.Deadline;
-import okio.OkBuffer;
+import okio.Buffer;
import okio.Sink;
+import okio.Timeout;
import static com.squareup.okhttp.internal.Util.checkOffsetAndCount;
@@ -30,10 +29,10 @@
* the post body to be transparently re-sent if the HTTP request must be
* sent multiple times.
*/
-final class RetryableSink implements Sink {
+public final class RetryableSink implements Sink {
private boolean closed;
private final int limit;
- private final OkBuffer content = new OkBuffer();
+ private final Buffer content = new Buffer();
public RetryableSink(int limit) {
this.limit = limit;
@@ -52,7 +51,7 @@
}
}
- @Override public void write(OkBuffer source, long byteCount) throws IOException {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
if (closed) throw new IllegalStateException("closed");
checkOffsetAndCount(source.size(), 0, byteCount);
if (limit != -1 && content.size() > limit - byteCount) {
@@ -64,16 +63,18 @@
@Override public void flush() throws IOException {
}
- @Override public Sink deadline(Deadline deadline) {
- return this;
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
}
public long contentLength() throws IOException {
return content.size();
}
- public void writeToSocket(BufferedSink socketOut) throws IOException {
- // Clone the content; otherwise we won't have data to retry.
- socketOut.write(content.clone(), content.size());
+ public void writeToSocket(Sink socketOut) throws IOException {
+ // Copy the content; otherwise we won't have data to retry.
+ Buffer buffer = new Buffer();
+ content.copyTo(buffer, 0, content.size());
+ socketOut.write(buffer, buffer.size());
}
}
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 876bac4..db9ae3b 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
@@ -16,178 +16,192 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
-import com.squareup.okhttp.HostResolver;
+import com.squareup.okhttp.ConnectionSpec;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
import com.squareup.okhttp.Route;
-import com.squareup.okhttp.RouteDatabase;
+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;
import java.net.InetSocketAddress;
import java.net.Proxy;
-import java.net.ProxySelector;
import java.net.SocketAddress;
+import java.net.SocketException;
import java.net.URI;
import java.net.UnknownHostException;
-import java.util.Iterator;
-import java.util.LinkedList;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLProtocolException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
/**
* Selects routes to connect to an origin server. Each connection requires a
- * choice of proxy server and IP address. Connections may also be recycled.
+ * choice of proxy server, IP address, and TLS mode. Connections may also be
+ * recycled.
*/
public final class RouteSelector {
-
private final Address address;
private final URI uri;
- private final ProxySelector proxySelector;
- private final ConnectionPool pool;
- private final HostResolver hostResolver;
+ private final Network network;
+ private final OkHttpClient client;
private final RouteDatabase routeDatabase;
+ private final Request request;
/* The most recently attempted route. */
private Proxy lastProxy;
private InetSocketAddress lastInetSocketAddress;
+ private ConnectionSpec lastSpec;
/* State for negotiating the next proxy to use. */
- private boolean hasNextProxy;
- private Proxy userSpecifiedProxy;
- private Iterator<Proxy> proxySelectorProxies;
+ private List<Proxy> proxies = Collections.emptyList();
+ private int nextProxyIndex;
- /* State for negotiating the next InetSocketAddress to use. */
- private InetAddress[] socketAddresses;
- private int nextSocketAddressIndex;
- private int socketPort;
+ /* State for negotiating the next socket address to use. */
+ private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
+ private int nextInetSocketAddressIndex;
+
+ /* Specs to attempt with the connection. */
+ private List<ConnectionSpec> connectionSpecs = Collections.emptyList();
+ private int nextSpecIndex;
/* State for negotiating failed routes */
- private final List<Route> postponedRoutes;
+ private final List<Route> postponedRoutes = new ArrayList<>();
- public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
- HostResolver hostResolver, RouteDatabase routeDatabase) {
+ private RouteSelector(Address address, URI uri, OkHttpClient client, Request request) {
this.address = address;
this.uri = uri;
- this.proxySelector = proxySelector;
- this.pool = pool;
- this.hostResolver = hostResolver;
- this.routeDatabase = routeDatabase;
- this.postponedRoutes = new LinkedList<Route>();
+ this.client = client;
+ this.routeDatabase = Internal.instance.routeDatabase(client);
+ this.network = Internal.instance.network(client);
+ this.request = request;
resetNextProxy(uri, address.getProxy());
}
+ public static RouteSelector get(Address address, Request request, OkHttpClient client)
+ throws IOException {
+ return new RouteSelector(address, request.uri(), client, request);
+ }
+
/**
* Returns true if there's another route to attempt. Every address has at
* least one route.
*/
public boolean hasNext() {
- return hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
+ return hasNextConnectionSpec()
+ || hasNextInetSocketAddress()
+ || hasNextProxy()
+ || hasNextPostponed();
}
- /**
- * Returns the next route address to attempt.
- *
- * @throws NoSuchElementException if there are no more routes to attempt.
- */
- public Connection next(String method) throws IOException {
- // Always prefer pooled connections over new connections.
- for (Connection pooled; (pooled = pool.get(address)) != null; ) {
- if (method.equals("GET") || pooled.isReadable()) return pooled;
- pooled.close();
- }
-
+ public Route next() throws IOException {
// Compute the next route to attempt.
- if (!hasNextInetSocketAddress()) {
- if (!hasNextProxy()) {
- if (!hasNextPostponed()) {
- throw new NoSuchElementException();
+ if (!hasNextConnectionSpec()) {
+ if (!hasNextInetSocketAddress()) {
+ if (!hasNextProxy()) {
+ if (!hasNextPostponed()) {
+ throw new NoSuchElementException();
+ }
+ return nextPostponed();
}
- return new Connection(pool, nextPostponed());
+ lastProxy = nextProxy();
}
- lastProxy = nextProxy();
- resetNextInetSocketAddress(lastProxy);
+ lastInetSocketAddress = nextInetSocketAddress();
}
- lastInetSocketAddress = nextInetSocketAddress();
+ lastSpec = nextConnectionSpec();
- Route route = new Route(address, lastProxy, lastInetSocketAddress);
+ final boolean shouldSendTlsFallbackIndicator = shouldSendTlsFallbackIndicator(lastSpec);
+ Route route = new Route(address, lastProxy, lastInetSocketAddress, lastSpec,
+ shouldSendTlsFallbackIndicator);
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
- // We will only recurse in order to skip previously failed routes. They will be
- // tried last.
- return next(method);
+ // We will only recurse in order to skip previously failed routes. They will be tried last.
+ return next();
}
- return new Connection(pool, route);
+ return route;
+ }
+
+ private boolean shouldSendTlsFallbackIndicator(ConnectionSpec connectionSpec) {
+ return connectionSpec != connectionSpecs.get(0)
+ && connectionSpec.isTls();
}
/**
* Clients should invoke this method when they encounter a connectivity
* failure on a connection returned by this route selector.
*/
- public void connectFailed(Connection connection, IOException failure) {
- // If this is a recycled connection, don't count its failure against the route.
- if (connection.recycleCount() > 0) return;
-
- Route failedRoute = connection.getRoute();
- if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
+ public void connectFailed(Route failedRoute, IOException failure) {
+ if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && address.getProxySelector() != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
- proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
+ address.getProxySelector().connectFailed(uri, failedRoute.getProxy().address(), failure);
}
routeDatabase.failed(failedRoute);
- }
- /** Resets {@link #nextProxy} to the first option. */
- private void resetNextProxy(URI uri, Proxy proxy) {
- this.hasNextProxy = true; // This includes NO_PROXY!
- if (proxy != null) {
- this.userSpecifiedProxy = proxy;
- } else {
- List<Proxy> proxyList = proxySelector.select(uri);
- if (proxyList != null) {
- this.proxySelectorProxies = proxyList.iterator();
+ // If the previously returned route's problem was not related to the connection's spec, and the
+ // next route only changes that, we shouldn't even attempt it. This suppresses it in both this
+ // selector and also in the route database.
+ if (!(failure instanceof SSLHandshakeException) && !(failure instanceof SSLProtocolException)) {
+ while (nextSpecIndex < connectionSpecs.size()) {
+ ConnectionSpec connectionSpec = connectionSpecs.get(nextSpecIndex++);
+ final boolean shouldSendTlsFallbackIndicator =
+ shouldSendTlsFallbackIndicator(connectionSpec);
+ Route toSuppress = new Route(address, lastProxy, lastInetSocketAddress, connectionSpec,
+ shouldSendTlsFallbackIndicator);
+ routeDatabase.failed(toSuppress);
}
}
}
+ /** Prepares the proxy servers to try. */
+ private void resetNextProxy(URI uri, Proxy proxy) {
+ if (proxy != null) {
+ // If the user specifies a proxy, try that and only that.
+ proxies = Collections.singletonList(proxy);
+ } else {
+ // 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(uri);
+ if (selectedProxies != null) proxies.addAll(selectedProxies);
+ // Finally try a direct connection. We only try it once!
+ proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));
+ proxies.add(Proxy.NO_PROXY);
+ }
+ nextProxyIndex = 0;
+ }
+
/** Returns true if there's another proxy to try. */
private boolean hasNextProxy() {
- return hasNextProxy;
+ return nextProxyIndex < proxies.size();
}
/** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
- private Proxy nextProxy() {
- // If the user specifies a proxy, try that and only that.
- if (userSpecifiedProxy != null) {
- hasNextProxy = false;
- return userSpecifiedProxy;
+ private Proxy nextProxy() throws IOException {
+ if (!hasNextProxy()) {
+ throw new SocketException("No route to " + address.getUriHost()
+ + "; exhausted proxy configurations: " + proxies);
}
-
- // Try each of the ProxySelector choices until one connection succeeds. If none succeed
- // then we'll try a direct connection below.
- if (proxySelectorProxies != null) {
- while (proxySelectorProxies.hasNext()) {
- Proxy candidate = proxySelectorProxies.next();
- if (candidate.type() != Proxy.Type.DIRECT) {
- return candidate;
- }
- }
- }
-
- // Finally try a direct connection.
- hasNextProxy = false;
- return Proxy.NO_PROXY;
+ Proxy result = proxies.get(nextProxyIndex++);
+ resetNextInetSocketAddress(result);
+ return result;
}
- /** Resets {@link #nextInetSocketAddress} to the first option. */
+ /** Prepares the socket addresses to attempt for the current proxy or host. */
private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
- socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
+ // Clear the addresses. Necessary if getAllByName() below throws!
+ inetSocketAddresses = new ArrayList<>();
String socketHost;
- if (proxy.type() == Proxy.Type.DIRECT) {
+ int socketPort;
+ if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
socketHost = address.getUriHost();
socketPort = getEffectivePort(uri);
} else {
@@ -202,8 +216,10 @@
}
// Try each address for best behavior in mixed IPv4/IPv6 environments.
- socketAddresses = hostResolver.getAllByName(socketHost);
- nextSocketAddressIndex = 0;
+ for (InetAddress inetAddress : network.resolveInetAddresses(socketHost)) {
+ inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+ }
+ nextInetSocketAddressIndex = 0;
}
/**
@@ -226,21 +242,47 @@
/** Returns true if there's another socket address to try. */
private boolean hasNextInetSocketAddress() {
- return socketAddresses != null;
+ return nextInetSocketAddressIndex < inetSocketAddresses.size();
}
/** Returns the next socket address to try. */
- private InetSocketAddress nextInetSocketAddress() throws UnknownHostException {
- InetSocketAddress result =
- new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort);
- if (nextSocketAddressIndex == socketAddresses.length) {
- socketAddresses = null; // So that hasNextInetSocketAddress() returns false.
- nextSocketAddressIndex = 0;
+ private InetSocketAddress nextInetSocketAddress() throws IOException {
+ if (!hasNextInetSocketAddress()) {
+ throw new SocketException("No route to " + address.getUriHost()
+ + "; exhausted inet socket addresses: " + inetSocketAddresses);
}
-
+ InetSocketAddress result = inetSocketAddresses.get(nextInetSocketAddressIndex++);
+ resetConnectionSpecs();
return result;
}
+ /** Prepares the connection specs to attempt. */
+ private void resetConnectionSpecs() {
+ connectionSpecs = new ArrayList<>();
+ List<ConnectionSpec> specs = address.getConnectionSpecs();
+ for (int i = 0, size = specs.size(); i < size; i++) {
+ ConnectionSpec spec = specs.get(i);
+ if (request.isHttps() == spec.isTls()) {
+ connectionSpecs.add(spec);
+ }
+ }
+ nextSpecIndex = 0;
+ }
+
+ /** Returns true if there's another connection spec to try. */
+ private boolean hasNextConnectionSpec() {
+ return nextSpecIndex < connectionSpecs.size();
+ }
+
+ /** Returns the next connection spec to try. */
+ private ConnectionSpec nextConnectionSpec() throws IOException {
+ if (!hasNextConnectionSpec()) {
+ throw new SocketException("No route to " + address.getUriHost()
+ + "; exhausted connection specs: " + connectionSpecs);
+ }
+ return connectionSpecs.get(nextSpecIndex++);
+ }
+
/** Returns true if there is another postponed route to try. */
private boolean hasNextPostponed() {
return !postponedRoutes.isEmpty();
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
index 9db9643..61b6610 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/SpdyTransport.java
@@ -20,26 +20,23 @@
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.spdy.ErrorCode;
import com.squareup.okhttp.internal.spdy.Header;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import com.squareup.okhttp.internal.spdy.SpdyStream;
import java.io.IOException;
-import java.io.OutputStream;
-import java.net.CacheRequest;
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.Deadline;
-import okio.OkBuffer;
import okio.Okio;
import okio.Sink;
-import okio.Source;
import static com.squareup.okhttp.internal.spdy.Header.RESPONSE_STATUS;
import static com.squareup.okhttp.internal.spdy.Header.TARGET_AUTHORITY;
@@ -78,9 +75,7 @@
this.spdyConnection = spdyConnection;
}
- @Override public Sink createRequestBody(Request request) throws IOException {
- // TODO: if bufferRequestBody is set, we must buffer the whole request
- writeRequestHeaders(request);
+ @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
return stream.getSink();
}
@@ -88,20 +83,20 @@
if (stream != null) return;
httpEngine.writingRequestHeaders();
- boolean hasRequestBody = httpEngine.hasRequestBody();
+ boolean permitsRequestBody = httpEngine.permitsRequestBody();
boolean hasResponseBody = true;
- String version = RequestLine.version(httpEngine.getConnection().getHttpMinorVersion());
+ String version = RequestLine.version(httpEngine.getConnection().getProtocol());
stream = spdyConnection.newStream(
- writeNameValueBlock(request, spdyConnection.getProtocol(), version), hasRequestBody,
+ writeNameValueBlock(request, spdyConnection.getProtocol(), version), permitsRequestBody,
hasResponseBody);
- stream.setReadTimeout(httpEngine.client.getReadTimeout());
+ stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
}
@Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
- throw new UnsupportedOperationException();
+ requestBody.writeToSocket(stream.getSink());
}
- @Override public void flushRequest() throws IOException {
+ @Override public void finishRequest() throws IOException {
stream.getSink().close();
}
@@ -117,8 +112,7 @@
public static List<Header> writeNameValueBlock(Request request, Protocol protocol,
String version) {
Headers headers = request.headers();
- // TODO: make the known header names constants.
- List<Header> result = new ArrayList<Header>(headers.size() + 10);
+ 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.url())));
String host = HttpEngine.hostHeader(request.url());
@@ -126,14 +120,14 @@
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));
+ result.add(new Header(TARGET_AUTHORITY, host)); // Optional in HTTP/2
} else {
throw new AssertionError();
}
result.add(new Header(TARGET_SCHEME, request.url().getProtocol()));
Set<ByteString> names = new LinkedHashSet<ByteString>();
- for (int i = 0; i < headers.size(); i++) {
+ 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);
@@ -180,8 +174,8 @@
String version = "HTTP/1.1"; // :version present only in spdy/3.
Headers.Builder headersBuilder = new Headers.Builder();
- headersBuilder.set(OkHeaders.SELECTED_PROTOCOL, protocol.name.utf8());
- for (int i = 0; i < headerBlock.size(); i++) {
+ 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(); ) {
@@ -201,26 +195,24 @@
}
}
if (status == null) throw new ProtocolException("Expected ':status' header not present");
- if (version == null) throw new ProtocolException("Expected ':version' header not present");
+ StatusLine statusLine = StatusLine.parse(version + " " + status);
return new Response.Builder()
- .statusLine(new StatusLine(version + " " + status))
+ .protocol(protocol)
+ .code(statusLine.code)
+ .message(statusLine.message)
.headers(headersBuilder.build());
}
- @Override public void emptyTransferStream() {
- // Do nothing.
- }
-
- @Override public Source getTransferStream(CacheRequest cacheRequest) throws IOException {
- return new SpdySource(stream, cacheRequest);
+ @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 {
- stream.close(ErrorCode.CANCEL);
+ if (stream != null) stream.close(ErrorCode.CANCEL);
}
@Override public boolean canReuseConnection() {
@@ -237,89 +229,4 @@
throw new AssertionError(protocol);
}
}
-
- /** An HTTP message body terminated by the end of the underlying stream. */
- private static class SpdySource implements Source {
- private final SpdyStream stream;
- private final Source source;
- private final CacheRequest cacheRequest;
- private final OutputStream cacheBody;
-
- private boolean inputExhausted;
- private boolean closed;
-
- SpdySource(SpdyStream stream, CacheRequest cacheRequest) throws IOException {
- this.stream = stream;
- this.source = stream.getSource();
-
- // Some apps return a null body; for compatibility we treat that like a null cache request.
- OutputStream cacheBody = cacheRequest != null ? cacheRequest.getBody() : null;
- if (cacheBody == null) {
- cacheRequest = null;
- }
-
- this.cacheBody = cacheBody;
- this.cacheRequest = cacheRequest;
- }
-
- @Override public long read(OkBuffer sink, long byteCount)
- throws IOException {
- if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
- if (closed) throw new IllegalStateException("closed");
- if (inputExhausted) return -1;
-
- long read = source.read(sink, byteCount);
- if (read == -1) {
- inputExhausted = true;
- if (cacheRequest != null) {
- cacheBody.close();
- }
- return -1;
- }
-
- if (cacheBody != null) {
- Okio.copy(sink, sink.size() - read, read, cacheBody);
- }
-
- return read;
- }
-
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
-
- @Override public void close() throws IOException {
- if (closed) return;
-
- if (!inputExhausted && cacheBody != null) {
- discardStream(); // Could make inputExhausted true!
- }
-
- closed = true;
-
- if (!inputExhausted) {
- stream.closeLater(ErrorCode.CANCEL);
- if (cacheRequest != null) {
- cacheRequest.abort();
- }
- }
- }
-
- private boolean discardStream() {
- try {
- long socketTimeout = stream.getReadTimeoutMillis();
- stream.setReadTimeout(socketTimeout);
- stream.setReadTimeout(DISCARD_STREAM_TIMEOUT_MILLIS);
- try {
- Util.skipAll(this, DISCARD_STREAM_TIMEOUT_MILLIS);
- return true;
- } finally {
- stream.setReadTimeout(socketTimeout);
- }
- } catch (IOException e) {
- return false;
- }
- }
- }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java
index d295891..ab9ebc1 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StatusLine.java
@@ -1,38 +1,54 @@
package com.squareup.okhttp.internal.http;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Response;
import java.io.IOException;
import java.net.ProtocolException;
+/** An HTTP response status line like "HTTP/1.1 200 OK". */
public final class StatusLine {
/** Numeric status code, 307: Temporary Redirect. */
public static final int HTTP_TEMP_REDIRECT = 307;
+ public static final int HTTP_PERM_REDIRECT = 308;
public static final int HTTP_CONTINUE = 100;
- private final String statusLine;
- private final int httpMinorVersion;
- private final int responseCode;
- private final String responseMessage;
+ public final Protocol protocol;
+ public final int code;
+ public final String message;
- /** Sets the response status line (like "HTTP/1.0 200 OK"). */
- public StatusLine(String statusLine) throws IOException {
+ public StatusLine(Protocol protocol, int code, String message) {
+ this.protocol = protocol;
+ this.code = code;
+ this.message = message;
+ }
+
+ public static StatusLine get(Response response) {
+ return new StatusLine(response.protocol(), response.code(), response.message());
+ }
+
+ public static StatusLine parse(String statusLine) throws IOException {
// H T T P / 1 . 1 2 0 0 T e m p o r a r y R e d i r e c t
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
// Parse protocol like "HTTP/1.1" followed by a space.
int codeStart;
- int httpMinorVersion;
+ Protocol protocol;
if (statusLine.startsWith("HTTP/1.")) {
if (statusLine.length() < 9 || statusLine.charAt(8) != ' ') {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
- httpMinorVersion = statusLine.charAt(7) - '0';
+ int httpMinorVersion = statusLine.charAt(7) - '0';
codeStart = 9;
- if (httpMinorVersion < 0 || httpMinorVersion > 9) {
+ if (httpMinorVersion == 0) {
+ protocol = Protocol.HTTP_1_0;
+ } else if (httpMinorVersion == 1) {
+ protocol = Protocol.HTTP_1_1;
+ } else {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
} else if (statusLine.startsWith("ICY ")) {
// Shoutcast uses ICY instead of "HTTP/1.0".
- httpMinorVersion = 0;
+ protocol = Protocol.HTTP_1_0;
codeStart = 4;
} else {
throw new ProtocolException("Unexpected status line: " + statusLine);
@@ -42,48 +58,33 @@
if (statusLine.length() < codeStart + 3) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
- int responseCode;
+ int code;
try {
- responseCode = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3));
+ code = Integer.parseInt(statusLine.substring(codeStart, codeStart + 3));
} catch (NumberFormatException e) {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
// Parse an optional response message like "OK" or "Not Modified". If it
// exists, it is separated from the response code by a space.
- String responseMessage = "";
+ String message = "";
if (statusLine.length() > codeStart + 3) {
if (statusLine.charAt(codeStart + 3) != ' ') {
throw new ProtocolException("Unexpected status line: " + statusLine);
}
- responseMessage = statusLine.substring(codeStart + 4);
+ message = statusLine.substring(codeStart + 4);
}
- this.responseMessage = responseMessage;
- this.responseCode = responseCode;
- this.statusLine = statusLine;
- this.httpMinorVersion = httpMinorVersion;
+ return new StatusLine(protocol, code, message);
}
- public String getStatusLine() {
- return statusLine;
- }
-
- /**
- * Returns the status line's HTTP minor version. This returns 0 for HTTP/1.0
- * and 1 for HTTP/1.1. This returns 1 if the HTTP version is unknown.
- */
- public int httpMinorVersion() {
- return httpMinorVersion != -1 ? httpMinorVersion : 1;
- }
-
- /** Returns the HTTP status code or -1 if it is unknown. */
- public int code() {
- return responseCode;
- }
-
- /** Returns the HTTP status message or null if it is unknown. */
- public String message() {
- return responseMessage;
+ @Override public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append(protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1");
+ result.append(' ').append(code);
+ if (message != null) {
+ result.append(' ').append(message);
+ }
+ return result.toString();
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
index 852a15b..77f7c9e 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
@@ -18,12 +18,11 @@
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
-import java.net.CacheRequest;
import okio.Sink;
-import okio.Source;
-interface Transport {
+public interface Transport {
/**
* 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
@@ -31,23 +30,8 @@
*/
int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
- /**
- * Returns an output stream where the request body can be written. The
- * returned stream will of one of two types:
- * <ul>
- * <li><strong>Direct.</strong> Bytes are written to the socket and
- * forgotten. This is most efficient, particularly for large request
- * bodies. The returned stream may be buffered; the caller must call
- * {@link #flushRequest} before reading the response.</li>
- * <li><strong>Buffered.</strong> Bytes are written to an in memory
- * buffer, and must be explicitly flushed with a call to {@link
- * #writeRequestBody}. This allows HTTP authorization (401, 407)
- * responses to be retransmitted transparently.</li>
- * </ul>
- */
- // TODO: don't bother retransmitting the request body? It's quite a corner
- // case and there's uncertainty whether Firefox or Chrome do this
- Sink createRequestBody(Request request) throws IOException;
+ /** Returns an output stream where the request body can be streamed. */
+ Sink createRequestBody(Request request, long contentLength) throws IOException;
/** This should update the HTTP engine's sentRequestMillis field. */
void writeRequestHeaders(Request request) throws IOException;
@@ -58,17 +42,14 @@
*/
void writeRequestBody(RetryableSink requestBody) throws IOException;
- /** Flush the request body to the underlying socket. */
- void flushRequest() throws IOException;
+ /** Flush the request to the underlying socket. */
+ void finishRequest() throws IOException;
- /** Read response headers and update the cookie manager. */
+ /** Read and return response headers. */
Response.Builder readResponseHeaders() throws IOException;
- /** Notify the transport that no response body will be read. */
- void emptyTransferStream() throws IOException;
-
- // TODO: make this the content stream?
- Source getTransferStream(CacheRequest cacheRequest) throws IOException;
+ /** 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
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
index 045677b..b5f46b8 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/ErrorCode.java
@@ -1,7 +1,21 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.squareup.okhttp.internal.spdy;
-// TODO: revisit for http/2 draft 9
-// http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-7
+// http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-7
public enum ErrorCode {
/** Not an error! For SPDY stream resets, prefer null over NO_ERROR. */
NO_ERROR(0, -1, 0),
@@ -34,6 +48,14 @@
COMPRESSION_ERROR(9, -1, -1),
+ CONNECT_ERROR(10, -1, -1),
+
+ ENHANCE_YOUR_CALM(11, -1, -1),
+
+ INADEQUATE_SECURITY(12, -1, -1),
+
+ HTTP_1_1_REQUIRED(13, -1, -1),
+
INVALID_CREDENTIALS(-1, 10, -1);
public final int httpCode;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
index c87226a..e3736c5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameReader.java
@@ -24,7 +24,7 @@
/** Reads transport frames for SPDY/3 or HTTP/2. */
public interface FrameReader extends Closeable {
- void readConnectionHeader() throws IOException;
+ void readConnectionPreface() throws IOException;
boolean nextFrame(Handler handler) throws IOException;
public interface Handler {
@@ -40,13 +40,10 @@
* @param inFinished true if the sender will not send further frames.
* @param streamId the stream owning these headers.
* @param associatedStreamId the stream that triggered the sender to create
- * this stream.
- * @param priority or -1 for no priority. For SPDY, priorities range from 0
- * (highest) thru 7 (lowest). For HTTP/2, priorities range from 0
- * (highest) thru 2^31-1 (lowest), defaulting to 2^30.
+ * this stream.
*/
void headers(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
- int priority, List<Header> headerBlock, HeadersMode headersMode);
+ List<Header> headerBlock, HeadersMode headersMode);
void rstStream(int streamId, ErrorCode errorCode);
void settings(boolean clearPrevious, Settings settings);
@@ -76,7 +73,7 @@
* sending this message. If {@code lastGoodStreamId} is zero, the peer
* processed no frames.
* @param errorCode reason for closing the connection.
- * @param debugData only valid for http/2; opaque debug data to send.
+ * @param debugData only valid for HTTP/2; opaque debug data to send.
*/
void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData);
@@ -85,7 +82,18 @@
* sent on {@code streamId}, or the connection if {@code streamId} is zero.
*/
void windowUpdate(int streamId, long windowSizeIncrement);
- void priority(int streamId, int priority);
+
+ /**
+ * Called when reading a headers or priority frame. This may be used to
+ * change the stream's weight from the default (16) to a new value.
+ *
+ * @param streamId stream which has a priority change.
+ * @param streamDependency the stream ID this stream is dependent on.
+ * @param weight relative proportion of priority in [1..256].
+ * @param exclusive inserts this stream ID as the sole child of
+ * {@code streamDependency}.
+ */
+ void priority(int streamId, int streamDependency, int weight, boolean exclusive);
/**
* HTTP/2 only. Receive a push promise header block.
@@ -93,8 +101,7 @@
* A push promise contains all the headers that pertain to a server-initiated
* request, and a {@code promisedStreamId} to which response frames will be
* delivered. Push promise frames are sent as a part of the response to
- * {@code streamId}. The {@code promisedStreamId} has a priority of one
- * greater than {@code streamId}.
+ * {@code streamId}.
*
* @param streamId client-initiated stream ID. Must be an odd number.
* @param promisedStreamId server-initiated stream ID. Must be an even
@@ -104,5 +111,27 @@
*/
void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders)
throws IOException;
+
+ /**
+ * HTTP/2 only. Expresses that resources for the connection or a client-
+ * initiated stream are available from a different network location or
+ * protocol configuration.
+ *
+ * <p>See <a href="http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-01">alt-svc</a>
+ *
+ * @param streamId when a client-initiated stream ID (odd number), the
+ * origin of this alternate service is the origin of the stream. When
+ * zero, the origin is specified in the {@code origin} parameter.
+ * @param origin when present, the
+ * <a href="http://tools.ietf.org/html/rfc6454">origin</a> is typically
+ * represented as a combination of scheme, host and port. When empty,
+ * the origin is that of the {@code streamId}.
+ * @param protocol an ALPN protocol, such as {@code h2}.
+ * @param host an IP address or hostname.
+ * @param port the IP port associated with the service.
+ * @param maxAge time in seconds that this alternative is considered fresh.
+ */
+ void alternateService(int streamId, String origin, ByteString protocol, String host, int port,
+ long maxAge);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
index f96c2aa..0f4b799 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/FrameWriter.java
@@ -19,13 +19,14 @@
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
-import okio.OkBuffer;
+import okio.Buffer;
/** Writes transport frames for SPDY/3 or HTTP/2. */
public interface FrameWriter extends Closeable {
/** HTTP/2 only. */
- void connectionHeader() throws IOException;
- void ackSettings() throws IOException;
+ void connectionPreface() throws IOException;
+ /** Informs the peer that we've applied its latest settings. */
+ void ackSettings(Settings peerSettings) throws IOException;
/**
* HTTP/2 only. Send a push promise header block.
@@ -48,21 +49,24 @@
/** SPDY/3 only. */
void flush() throws IOException;
void synStream(boolean outFinished, boolean inFinished, int streamId, int associatedStreamId,
- int priority, int slot, List<Header> headerBlock) throws IOException;
+ List<Header> headerBlock) throws IOException;
void synReply(boolean outFinished, int streamId, List<Header> headerBlock)
throws IOException;
void headers(int streamId, List<Header> headerBlock) throws IOException;
void rstStream(int streamId, ErrorCode errorCode) throws IOException;
+ /** The maximum size of bytes that may be sent in a single call to {@link #data}. */
+ int maxDataLength();
+
/**
- * {@code data.length} may be longer than the max length of the variant's data frame.
+ * {@code source.length} may be longer than the max length of the variant's data frame.
* Implementations must send multiple frames as necessary.
*
* @param source the buffer to draw bytes from. May be null if byteCount is 0.
+ * @param byteCount must be between 0 and the minimum of {code source.length}
+ * and {@link #maxDataLength}.
*/
- void data(boolean outFinished, int streamId, OkBuffer source, int byteCount) throws IOException;
-
- void data(boolean outFinished, int streamId, OkBuffer source) throws IOException;
+ void data(boolean outFinished, int streamId, Buffer source, int byteCount) throws IOException;
/** Write okhttp's settings to the peer. */
void settings(Settings okHttpSettings) throws IOException;
@@ -87,7 +91,7 @@
* @param lastGoodStreamId the last stream ID processed, or zero if no
* streams were processed.
* @param errorCode reason for closing the connection.
- * @param debugData only valid for http/2; opaque debug data to send.
+ * @param debugData only valid for HTTP/2; opaque debug data to send.
*/
void goAway(int lastGoodStreamId, ErrorCode errorCode, byte[] debugData) throws IOException;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
index 1e9b503..d14d131 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Header.java
@@ -9,7 +9,7 @@
public static final ByteString TARGET_METHOD = ByteString.encodeUtf8(":method");
public static final ByteString TARGET_PATH = ByteString.encodeUtf8(":path");
public static final ByteString TARGET_SCHEME = ByteString.encodeUtf8(":scheme");
- public static final ByteString TARGET_AUTHORITY = ByteString.encodeUtf8(":authority"); // http/2
+ public static final ByteString TARGET_AUTHORITY = ByteString.encodeUtf8(":authority"); // HTTP/2
public static final ByteString TARGET_HOST = ByteString.encodeUtf8(":host"); // spdy/3
public static final ByteString VERSION = ByteString.encodeUtf8(":version"); // spdy/3
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
index e16e176..c06327a 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HeadersMode.java
@@ -15,7 +15,7 @@
*/
package com.squareup.okhttp.internal.spdy;
-enum HeadersMode {
+public enum HeadersMode {
SPDY_SYN_STREAM,
SPDY_REPLY,
SPDY_HEADERS,
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java
deleted file mode 100644
index 645f162..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft05.java
+++ /dev/null
@@ -1,448 +0,0 @@
-package com.squareup.okhttp.internal.spdy;
-
-import com.squareup.okhttp.internal.BitArray;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import okio.BufferedSource;
-import okio.ByteString;
-import okio.OkBuffer;
-import okio.Okio;
-import okio.Source;
-
-/**
- * Read and write HPACK v05.
- *
- * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05
- *
- * This implementation uses an array for the header table with a bitset for
- * references. Dynamic entries are added to the array, starting in the last
- * position moving forward. When the array fills, it is doubled.
- */
-final class HpackDraft05 {
- private static final int PREFIX_6_BITS = 0x3f;
- private static final int PREFIX_7_BITS = 0x7f;
-
- private static final Header[] STATIC_HEADER_TABLE = new Header[] {
- new Header(Header.TARGET_AUTHORITY, ""),
- new Header(Header.TARGET_METHOD, "GET"),
- new Header(Header.TARGET_METHOD, "POST"),
- new Header(Header.TARGET_PATH, "/"),
- new Header(Header.TARGET_PATH, "/index.html"),
- new Header(Header.TARGET_SCHEME, "http"),
- new Header(Header.TARGET_SCHEME, "https"),
- new Header(Header.RESPONSE_STATUS, "200"),
- new Header(Header.RESPONSE_STATUS, "500"),
- new Header(Header.RESPONSE_STATUS, "404"),
- new Header(Header.RESPONSE_STATUS, "403"),
- new Header(Header.RESPONSE_STATUS, "400"),
- new Header(Header.RESPONSE_STATUS, "401"),
- new Header("accept-charset", ""),
- new Header("accept-encoding", ""),
- new Header("accept-language", ""),
- new Header("accept-ranges", ""),
- new Header("accept", ""),
- new Header("access-control-allow-origin", ""),
- new Header("age", ""),
- new Header("allow", ""),
- new Header("authorization", ""),
- new Header("cache-control", ""),
- new Header("content-disposition", ""),
- new Header("content-encoding", ""),
- new Header("content-language", ""),
- new Header("content-length", ""),
- new Header("content-location", ""),
- new Header("content-range", ""),
- new Header("content-type", ""),
- new Header("cookie", ""),
- new Header("date", ""),
- new Header("etag", ""),
- new Header("expect", ""),
- new Header("expires", ""),
- new Header("from", ""),
- new Header("host", ""),
- new Header("if-match", ""),
- new Header("if-modified-since", ""),
- new Header("if-none-match", ""),
- new Header("if-range", ""),
- new Header("if-unmodified-since", ""),
- new Header("last-modified", ""),
- new Header("link", ""),
- new Header("location", ""),
- new Header("max-forwards", ""),
- new Header("proxy-authenticate", ""),
- new Header("proxy-authorization", ""),
- new Header("range", ""),
- new Header("referer", ""),
- new Header("refresh", ""),
- new Header("retry-after", ""),
- new Header("server", ""),
- new Header("set-cookie", ""),
- new Header("strict-transport-security", ""),
- new Header("transfer-encoding", ""),
- new Header("user-agent", ""),
- new Header("vary", ""),
- new Header("via", ""),
- new Header("www-authenticate", "")
- };
-
- private HpackDraft05() {
- }
-
- // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.2
- static final class Reader {
- private final Huffman.Codec huffmanCodec;
-
- private final List<Header> emittedHeaders = new ArrayList<Header>();
- private final BufferedSource source;
- private int maxHeaderTableByteCount;
-
- // Visible for testing.
- Header[] headerTable = new Header[8];
- // Array is populated back to front, so new entries always have lowest index.
- int nextHeaderIndex = headerTable.length - 1;
- int headerCount = 0;
-
- /**
- * Set bit positions indicate {@code headerTable[pos]} should be emitted.
- */
- // Using a BitArray as it has left-shift operator.
- BitArray referencedHeaders = new BitArray.FixedCapacity();
-
- /**
- * Set bit positions indicate {@code headerTable[pos]} was already emitted.
- */
- BitArray emittedReferencedHeaders = new BitArray.FixedCapacity();
- int headerTableByteCount = 0;
-
- Reader(boolean client, int maxHeaderTableByteCount, Source source) {
- this.huffmanCodec = client ? Huffman.Codec.RESPONSE : Huffman.Codec.REQUEST;
- this.maxHeaderTableByteCount = maxHeaderTableByteCount;
- this.source = Okio.buffer(source);
- }
-
- int maxHeaderTableByteCount() {
- return maxHeaderTableByteCount;
- }
-
- /**
- * Called by the reader when the peer sent a new header table size setting.
- * <p>
- * Evicts entries or clears the table as needed.
- */
- void maxHeaderTableByteCount(int newMaxHeaderTableByteCount) {
- this.maxHeaderTableByteCount = newMaxHeaderTableByteCount;
- if (maxHeaderTableByteCount < headerTableByteCount) {
- if (maxHeaderTableByteCount == 0) {
- clearHeaderTable();
- } else {
- evictToRecoverBytes(headerTableByteCount - maxHeaderTableByteCount);
- }
- }
- }
-
- private void clearHeaderTable() {
- clearReferenceSet();
- Arrays.fill(headerTable, null);
- nextHeaderIndex = headerTable.length - 1;
- headerCount = 0;
- headerTableByteCount = 0;
- }
-
- /** Returns the count of entries evicted. */
- private int evictToRecoverBytes(int bytesToRecover) {
- int entriesToEvict = 0;
- if (bytesToRecover > 0) {
- // determine how many headers need to be evicted.
- for (int j = headerTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
- bytesToRecover -= headerTable[j].hpackSize;
- headerTableByteCount -= headerTable[j].hpackSize;
- headerCount--;
- entriesToEvict++;
- }
- referencedHeaders.shiftLeft(entriesToEvict);
- emittedReferencedHeaders.shiftLeft(entriesToEvict);
- System.arraycopy(headerTable, nextHeaderIndex + 1, headerTable,
- nextHeaderIndex + 1 + entriesToEvict, headerCount);
- nextHeaderIndex += entriesToEvict;
- }
- return entriesToEvict;
- }
-
- /**
- * Read {@code byteCount} bytes of headers from the source stream into the
- * set of emitted headers.
- */
- void readHeaders() throws IOException {
- while (!source.exhausted()) {
- int b = source.readByte() & 0xff;
- if (b == 0x80) { // 10000000
- clearReferenceSet();
- } else if ((b & 0x80) == 0x80) { // 1NNNNNNN
- int index = readInt(b, PREFIX_7_BITS);
- readIndexedHeader(index - 1);
- } else { // 0NNNNNNN
- if (b == 0x40) { // 01000000
- readLiteralHeaderWithoutIndexingNewName();
- } else if ((b & 0x40) == 0x40) { // 01NNNNNN
- int index = readInt(b, PREFIX_6_BITS);
- readLiteralHeaderWithoutIndexingIndexedName(index - 1);
- } else if (b == 0) { // 00000000
- readLiteralHeaderWithIncrementalIndexingNewName();
- } else if ((b & 0xc0) == 0) { // 00NNNNNN
- int index = readInt(b, PREFIX_6_BITS);
- readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
- } else {
- // TODO: we should throw something that we can coerce to a PROTOCOL_ERROR
- throw new AssertionError("unhandled byte: " + Integer.toBinaryString(b));
- }
- }
- }
- }
-
- private void clearReferenceSet() {
- referencedHeaders.clear();
- emittedReferencedHeaders.clear();
- }
-
- void emitReferenceSet() {
- for (int i = headerTable.length - 1; i != nextHeaderIndex; --i) {
- if (referencedHeaders.get(i) && !emittedReferencedHeaders.get(i)) {
- emittedHeaders.add(headerTable[i]);
- }
- }
- }
-
- /**
- * Returns all headers emitted since they were last cleared, then clears the
- * emitted headers.
- */
- List<Header> getAndReset() {
- List<Header> result = new ArrayList<Header>(emittedHeaders);
- emittedHeaders.clear();
- emittedReferencedHeaders.clear();
- return result;
- }
-
- private void readIndexedHeader(int index) {
- if (isStaticHeader(index)) {
- Header staticEntry = STATIC_HEADER_TABLE[index - headerCount];
- if (maxHeaderTableByteCount == 0) {
- emittedHeaders.add(staticEntry);
- } else {
- insertIntoHeaderTable(-1, staticEntry);
- }
- } else {
- int headerTableIndex = headerTableIndex(index);
- if (!referencedHeaders.get(headerTableIndex)) { // When re-referencing, emit immediately.
- emittedHeaders.add(headerTable[headerTableIndex]);
- emittedReferencedHeaders.set(headerTableIndex);
- }
- referencedHeaders.toggle(headerTableIndex);
- }
- }
-
- // referencedHeaders is relative to nextHeaderIndex + 1.
- private int headerTableIndex(int index) {
- return nextHeaderIndex + 1 + index;
- }
-
- private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException {
- ByteString name = getName(index);
- ByteString value = readByteString(false);
- emittedHeaders.add(new Header(name, value));
- }
-
- private void readLiteralHeaderWithoutIndexingNewName() throws IOException {
- ByteString name = readByteString(true);
- ByteString value = readByteString(false);
- emittedHeaders.add(new Header(name, value));
- }
-
- private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
- throws IOException {
- ByteString name = getName(nameIndex);
- ByteString value = readByteString(false);
- insertIntoHeaderTable(-1, new Header(name, value));
- }
-
- private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
- ByteString name = readByteString(true);
- ByteString value = readByteString(false);
- insertIntoHeaderTable(-1, new Header(name, value));
- }
-
- private ByteString getName(int index) {
- if (isStaticHeader(index)) {
- return STATIC_HEADER_TABLE[index - headerCount].name;
- } else {
- return headerTable[headerTableIndex(index)].name;
- }
- }
-
- private boolean isStaticHeader(int index) {
- return index >= headerCount;
- }
-
- /** index == -1 when new. */
- private void insertIntoHeaderTable(int index, Header entry) {
- int delta = entry.hpackSize;
- if (index != -1) { // Index -1 == new header.
- delta -= headerTable[headerTableIndex(index)].hpackSize;
- }
-
- // if the new or replacement header is too big, drop all entries.
- if (delta > maxHeaderTableByteCount) {
- clearHeaderTable();
- // emit the large header to the callback.
- emittedHeaders.add(entry);
- return;
- }
-
- // Evict headers to the required length.
- int bytesToRecover = (headerTableByteCount + delta) - maxHeaderTableByteCount;
- int entriesEvicted = evictToRecoverBytes(bytesToRecover);
-
- if (index == -1) { // Adding a value to the header table.
- if (headerCount + 1 > headerTable.length) { // Need to grow the header table.
- Header[] doubled = new Header[headerTable.length * 2];
- System.arraycopy(headerTable, 0, doubled, headerTable.length, headerTable.length);
- if (doubled.length == 64) {
- referencedHeaders = ((BitArray.FixedCapacity) referencedHeaders).toVariableCapacity();
- emittedReferencedHeaders =
- ((BitArray.FixedCapacity) emittedReferencedHeaders).toVariableCapacity();
- }
- referencedHeaders.shiftLeft(headerTable.length);
- emittedReferencedHeaders.shiftLeft(headerTable.length);
- nextHeaderIndex = headerTable.length - 1;
- headerTable = doubled;
- }
- index = nextHeaderIndex--;
- referencedHeaders.set(index);
- headerTable[index] = entry;
- headerCount++;
- } else { // Replace value at same position.
- index += headerTableIndex(index) + entriesEvicted;
- referencedHeaders.set(index);
- headerTable[index] = entry;
- }
- headerTableByteCount += delta;
- }
-
- private int readByte() throws IOException {
- return source.readByte() & 0xff;
- }
-
- int readInt(int firstByte, int prefixMask) throws IOException {
- int prefix = firstByte & prefixMask;
- if (prefix < prefixMask) {
- return prefix; // This was a single byte value.
- }
-
- // This is a multibyte value. Read 7 bits at a time.
- int result = prefixMask;
- int shift = 0;
- while (true) {
- int b = readByte();
- if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
- result += (b & 0x7f) << shift;
- shift += 7;
- } else {
- result += b << shift; // Last byte.
- break;
- }
- }
- return result;
- }
-
- /**
- * Reads a potentially Huffman encoded string byte string. When
- * {@code asciiLowercase} is true, bytes will be converted to lowercase.
- */
- ByteString readByteString(boolean asciiLowercase) throws IOException {
- int firstByte = readByte();
- boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN
- int length = readInt(firstByte, PREFIX_7_BITS);
-
- ByteString byteString = source.readByteString(length);
-
- if (huffmanDecode) {
- byteString = huffmanCodec.decode(byteString); // TODO: streaming Huffman!
- }
-
- if (asciiLowercase) {
- byteString = byteString.toAsciiLowercase();
- }
-
- return byteString;
- }
- }
-
- private static final Map<ByteString, Integer> NAME_TO_FIRST_INDEX = nameToFirstIndex();
-
- private static Map<ByteString, Integer> nameToFirstIndex() {
- Map<ByteString, Integer> result =
- new LinkedHashMap<ByteString, Integer>(STATIC_HEADER_TABLE.length);
- for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) {
- if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) {
- result.put(STATIC_HEADER_TABLE[i].name, i);
- }
- }
- return Collections.unmodifiableMap(result);
- }
-
- static final class Writer {
- private final OkBuffer out;
-
- Writer(OkBuffer out) {
- this.out = out;
- }
-
- void writeHeaders(List<Header> headerBlock) throws IOException {
- // TODO: implement index tracking
- for (int i = 0, size = headerBlock.size(); i < size; i++) {
- ByteString name = headerBlock.get(i).name;
- Integer staticIndex = NAME_TO_FIRST_INDEX.get(name);
- if (staticIndex != null) {
- // Literal Header Field without Indexing - Indexed Name.
- writeInt(staticIndex + 1, PREFIX_6_BITS, 0x40);
- writeByteString(headerBlock.get(i).value);
- } else {
- out.writeByte(0x40); // Literal Header without Indexing - New Name.
- writeByteString(name);
- writeByteString(headerBlock.get(i).value);
- }
- }
- }
-
- // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4.1.1
- void writeInt(int value, int prefixMask, int bits) throws IOException {
- // Write the raw value for a single byte value.
- if (value < prefixMask) {
- out.writeByte(bits | value);
- return;
- }
-
- // Write the mask to start a multibyte value.
- out.writeByte(bits | prefixMask);
- value -= prefixMask;
-
- // Write 7 bits at a time 'til we're done.
- while (value >= 0x80) {
- int b = value & 0x7f;
- out.writeByte(b | 0x80);
- value >>>= 7;
- }
- out.writeByte(value);
- }
-
- void writeByteString(ByteString data) throws IOException {
- writeInt(data.size(), PREFIX_7_BITS, 0);
- out.write(data);
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft10.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft10.java
new file mode 100644
index 0000000..2e78015
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/HpackDraft10.java
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.Okio;
+import okio.Source;
+
+/**
+ * Read and write HPACK v10.
+ *
+ * http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10
+ *
+ * This implementation uses an array for the dynamic table and a list for
+ * indexed entries. Dynamic entries are added to the array, starting in the
+ * last position moving forward. When the array fills, it is doubled.
+ */
+final class HpackDraft10 {
+ private static final int PREFIX_4_BITS = 0x0f;
+ private static final int PREFIX_5_BITS = 0x1f;
+ private static final int PREFIX_6_BITS = 0x3f;
+ private static final int PREFIX_7_BITS = 0x7f;
+
+ private static final Header[] STATIC_HEADER_TABLE = new Header[] {
+ new Header(Header.TARGET_AUTHORITY, ""),
+ new Header(Header.TARGET_METHOD, "GET"),
+ new Header(Header.TARGET_METHOD, "POST"),
+ new Header(Header.TARGET_PATH, "/"),
+ new Header(Header.TARGET_PATH, "/index.html"),
+ new Header(Header.TARGET_SCHEME, "http"),
+ new Header(Header.TARGET_SCHEME, "https"),
+ new Header(Header.RESPONSE_STATUS, "200"),
+ new Header(Header.RESPONSE_STATUS, "204"),
+ new Header(Header.RESPONSE_STATUS, "206"),
+ new Header(Header.RESPONSE_STATUS, "304"),
+ new Header(Header.RESPONSE_STATUS, "400"),
+ new Header(Header.RESPONSE_STATUS, "404"),
+ new Header(Header.RESPONSE_STATUS, "500"),
+ new Header("accept-charset", ""),
+ new Header("accept-encoding", "gzip, deflate"),
+ new Header("accept-language", ""),
+ new Header("accept-ranges", ""),
+ new Header("accept", ""),
+ new Header("access-control-allow-origin", ""),
+ new Header("age", ""),
+ new Header("allow", ""),
+ new Header("authorization", ""),
+ new Header("cache-control", ""),
+ new Header("content-disposition", ""),
+ new Header("content-encoding", ""),
+ new Header("content-language", ""),
+ new Header("content-length", ""),
+ new Header("content-location", ""),
+ new Header("content-range", ""),
+ new Header("content-type", ""),
+ new Header("cookie", ""),
+ new Header("date", ""),
+ new Header("etag", ""),
+ new Header("expect", ""),
+ new Header("expires", ""),
+ new Header("from", ""),
+ new Header("host", ""),
+ new Header("if-match", ""),
+ new Header("if-modified-since", ""),
+ new Header("if-none-match", ""),
+ new Header("if-range", ""),
+ new Header("if-unmodified-since", ""),
+ new Header("last-modified", ""),
+ new Header("link", ""),
+ new Header("location", ""),
+ new Header("max-forwards", ""),
+ new Header("proxy-authenticate", ""),
+ new Header("proxy-authorization", ""),
+ new Header("range", ""),
+ new Header("referer", ""),
+ new Header("refresh", ""),
+ new Header("retry-after", ""),
+ new Header("server", ""),
+ new Header("set-cookie", ""),
+ new Header("strict-transport-security", ""),
+ new Header("transfer-encoding", ""),
+ new Header("user-agent", ""),
+ new Header("vary", ""),
+ new Header("via", ""),
+ new Header("www-authenticate", "")
+ };
+
+ private HpackDraft10() {
+ }
+
+ // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-3.1
+ static final class Reader {
+
+ private final List<Header> headerList = new ArrayList<>();
+ private final BufferedSource source;
+
+ private int headerTableSizeSetting;
+ private int maxDynamicTableByteCount;
+ // Visible for testing.
+ Header[] dynamicTable = new Header[8];
+ // Array is populated back to front, so new entries always have lowest index.
+ int nextHeaderIndex = dynamicTable.length - 1;
+ int headerCount = 0;
+ int dynamicTableByteCount = 0;
+
+ Reader(int headerTableSizeSetting, Source source) {
+ this.headerTableSizeSetting = headerTableSizeSetting;
+ this.maxDynamicTableByteCount = headerTableSizeSetting;
+ this.source = Okio.buffer(source);
+ }
+
+ int maxDynamicTableByteCount() {
+ return maxDynamicTableByteCount;
+ }
+
+ /**
+ * Called by the reader when the peer sent {@link Settings#HEADER_TABLE_SIZE}.
+ * While this establishes the maximum dynamic table size, the
+ * {@link #maxDynamicTableByteCount} set during processing may limit the
+ * table size to a smaller amount.
+ * <p> Evicts entries or clears the table as needed.
+ */
+ void headerTableSizeSetting(int headerTableSizeSetting) {
+ this.headerTableSizeSetting = headerTableSizeSetting;
+ this.maxDynamicTableByteCount = headerTableSizeSetting;
+ adjustDynamicTableByteCount();
+ }
+
+ private void adjustDynamicTableByteCount() {
+ if (maxDynamicTableByteCount < dynamicTableByteCount) {
+ if (maxDynamicTableByteCount == 0) {
+ clearDynamicTable();
+ } else {
+ evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount);
+ }
+ }
+ }
+
+ private void clearDynamicTable() {
+ headerList.clear();
+ Arrays.fill(dynamicTable, null);
+ nextHeaderIndex = dynamicTable.length - 1;
+ headerCount = 0;
+ dynamicTableByteCount = 0;
+ }
+
+ /** Returns the count of entries evicted. */
+ private int evictToRecoverBytes(int bytesToRecover) {
+ int entriesToEvict = 0;
+ if (bytesToRecover > 0) {
+ // determine how many headers need to be evicted.
+ for (int j = dynamicTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
+ bytesToRecover -= dynamicTable[j].hpackSize;
+ dynamicTableByteCount -= dynamicTable[j].hpackSize;
+ headerCount--;
+ entriesToEvict++;
+ }
+ System.arraycopy(dynamicTable, nextHeaderIndex + 1, dynamicTable,
+ nextHeaderIndex + 1 + entriesToEvict, headerCount);
+ nextHeaderIndex += entriesToEvict;
+ }
+ return entriesToEvict;
+ }
+
+ /**
+ * Read {@code byteCount} bytes of headers from the source stream. This
+ * implementation does not propagate the never indexed flag of a header.
+ */
+ void readHeaders() throws IOException {
+ while (!source.exhausted()) {
+ int b = source.readByte() & 0xff;
+ if (b == 0x80) { // 10000000
+ throw new IOException("index == 0");
+ } else if ((b & 0x80) == 0x80) { // 1NNNNNNN
+ int index = readInt(b, PREFIX_7_BITS);
+ readIndexedHeader(index - 1);
+ } else if (b == 0x40) { // 01000000
+ readLiteralHeaderWithIncrementalIndexingNewName();
+ } else if ((b & 0x40) == 0x40) { // 01NNNNNN
+ int index = readInt(b, PREFIX_6_BITS);
+ readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
+ } else if ((b & 0x20) == 0x20) { // 001NNNNN
+ maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS);
+ if (maxDynamicTableByteCount < 0
+ || maxDynamicTableByteCount > headerTableSizeSetting) {
+ throw new IOException("Invalid dynamic table size update " + maxDynamicTableByteCount);
+ }
+ adjustDynamicTableByteCount();
+ } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit.
+ readLiteralHeaderWithoutIndexingNewName();
+ } else { // 000?NNNN - Ignore never indexed bit.
+ int index = readInt(b, PREFIX_4_BITS);
+ readLiteralHeaderWithoutIndexingIndexedName(index - 1);
+ }
+ }
+ }
+
+ public List<Header> getAndResetHeaderList() {
+ List<Header> result = new ArrayList<>(headerList);
+ headerList.clear();
+ return result;
+ }
+
+ private void readIndexedHeader(int index) throws IOException {
+ if (isStaticHeader(index)) {
+ Header staticEntry = STATIC_HEADER_TABLE[index];
+ headerList.add(staticEntry);
+ } else {
+ int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length);
+ if (dynamicTableIndex < 0 || dynamicTableIndex > dynamicTable.length - 1) {
+ throw new IOException("Header index too large " + (index + 1));
+ }
+ headerList.add(dynamicTable[dynamicTableIndex]);
+ }
+ }
+
+ // referencedHeaders is relative to nextHeaderIndex + 1.
+ private int dynamicTableIndex(int index) {
+ return nextHeaderIndex + 1 + index;
+ }
+
+ private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException {
+ ByteString name = getName(index);
+ ByteString value = readByteString();
+ headerList.add(new Header(name, value));
+ }
+
+ private void readLiteralHeaderWithoutIndexingNewName() throws IOException {
+ ByteString name = checkLowercase(readByteString());
+ ByteString value = readByteString();
+ headerList.add(new Header(name, value));
+ }
+
+ private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
+ throws IOException {
+ ByteString name = getName(nameIndex);
+ ByteString value = readByteString();
+ insertIntoDynamicTable(-1, new Header(name, value));
+ }
+
+ private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
+ ByteString name = checkLowercase(readByteString());
+ ByteString value = readByteString();
+ insertIntoDynamicTable(-1, new Header(name, value));
+ }
+
+ private ByteString getName(int index) {
+ if (isStaticHeader(index)) {
+ return STATIC_HEADER_TABLE[index].name;
+ } else {
+ return dynamicTable[dynamicTableIndex(index - STATIC_HEADER_TABLE.length)].name;
+ }
+ }
+
+ private boolean isStaticHeader(int index) {
+ return index >= 0 && index <= STATIC_HEADER_TABLE.length - 1;
+ }
+
+ /** index == -1 when new. */
+ private void insertIntoDynamicTable(int index, Header entry) {
+ headerList.add(entry);
+
+ int delta = entry.hpackSize;
+ if (index != -1) { // Index -1 == new header.
+ delta -= dynamicTable[dynamicTableIndex(index)].hpackSize;
+ }
+
+ // if the new or replacement header is too big, drop all entries.
+ if (delta > maxDynamicTableByteCount) {
+ clearDynamicTable();
+ return;
+ }
+
+ // Evict headers to the required length.
+ int bytesToRecover = (dynamicTableByteCount + delta) - maxDynamicTableByteCount;
+ int entriesEvicted = evictToRecoverBytes(bytesToRecover);
+
+ if (index == -1) { // Adding a value to the dynamic table.
+ if (headerCount + 1 > dynamicTable.length) { // Need to grow the dynamic table.
+ Header[] doubled = new Header[dynamicTable.length * 2];
+ System.arraycopy(dynamicTable, 0, doubled, dynamicTable.length, dynamicTable.length);
+ nextHeaderIndex = dynamicTable.length - 1;
+ dynamicTable = doubled;
+ }
+ index = nextHeaderIndex--;
+ dynamicTable[index] = entry;
+ headerCount++;
+ } else { // Replace value at same position.
+ index += dynamicTableIndex(index) + entriesEvicted;
+ dynamicTable[index] = entry;
+ }
+ dynamicTableByteCount += delta;
+ }
+
+ private int readByte() throws IOException {
+ return source.readByte() & 0xff;
+ }
+
+ int readInt(int firstByte, int prefixMask) throws IOException {
+ int prefix = firstByte & prefixMask;
+ if (prefix < prefixMask) {
+ return prefix; // This was a single byte value.
+ }
+
+ // This is a multibyte value. Read 7 bits at a time.
+ int result = prefixMask;
+ int shift = 0;
+ while (true) {
+ int b = readByte();
+ if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
+ result += (b & 0x7f) << shift;
+ shift += 7;
+ } else {
+ result += b << shift; // Last byte.
+ break;
+ }
+ }
+ return result;
+ }
+
+ /** Reads a potentially Huffman encoded byte string. */
+ ByteString readByteString() throws IOException {
+ int firstByte = readByte();
+ boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN
+ int length = readInt(firstByte, PREFIX_7_BITS);
+
+ if (huffmanDecode) {
+ return ByteString.of(Huffman.get().decode(source.readByteArray(length)));
+ } else {
+ return source.readByteString(length);
+ }
+ }
+ }
+
+ private static final Map<ByteString, Integer> NAME_TO_FIRST_INDEX = nameToFirstIndex();
+
+ private static Map<ByteString, Integer> nameToFirstIndex() {
+ Map<ByteString, Integer> result = new LinkedHashMap<>(STATIC_HEADER_TABLE.length);
+ for (int i = 0; i < STATIC_HEADER_TABLE.length; i++) {
+ if (!result.containsKey(STATIC_HEADER_TABLE[i].name)) {
+ result.put(STATIC_HEADER_TABLE[i].name, i);
+ }
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ static final class Writer {
+ private final Buffer out;
+
+ Writer(Buffer out) {
+ this.out = out;
+ }
+
+ /** This does not use "never indexed" semantics for sensitive headers. */
+ // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-6.2.3
+ void writeHeaders(List<Header> headerBlock) throws IOException {
+ // TODO: implement index tracking
+ for (int i = 0, size = headerBlock.size(); i < size; i++) {
+ ByteString name = headerBlock.get(i).name.toAsciiLowercase();
+ Integer staticIndex = NAME_TO_FIRST_INDEX.get(name);
+ if (staticIndex != null) {
+ // Literal Header Field without Indexing - Indexed Name.
+ writeInt(staticIndex + 1, PREFIX_4_BITS, 0);
+ writeByteString(headerBlock.get(i).value);
+ } else {
+ out.writeByte(0x00); // Literal Header without Indexing - New Name.
+ writeByteString(name);
+ writeByteString(headerBlock.get(i).value);
+ }
+ }
+ }
+
+ // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#section-4.1.1
+ void writeInt(int value, int prefixMask, int bits) throws IOException {
+ // Write the raw value for a single byte value.
+ if (value < prefixMask) {
+ out.writeByte(bits | value);
+ return;
+ }
+
+ // Write the mask to start a multibyte value.
+ out.writeByte(bits | prefixMask);
+ value -= prefixMask;
+
+ // Write 7 bits at a time 'til we're done.
+ while (value >= 0x80) {
+ int b = value & 0x7f;
+ out.writeByte(b | 0x80);
+ value >>>= 7;
+ }
+ out.writeByte(value);
+ }
+
+ void writeByteString(ByteString data) throws IOException {
+ writeInt(data.size(), PREFIX_7_BITS, 0);
+ out.write(data);
+ }
+ }
+
+ /**
+ * An HTTP/2 response cannot contain uppercase header characters and must
+ * be treated as malformed.
+ */
+ private static ByteString checkLowercase(ByteString name) throws IOException {
+ for (int i = 0, length = name.size(); i < length; i++) {
+ byte c = name.getByte(i);
+ if (c >= 'A' && c <= 'Z') {
+ throw new IOException("PROTOCOL_ERROR response malformed: mixed case name: " + name.utf8());
+ }
+ }
+ return name;
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java
deleted file mode 100644
index a88b747..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft09.java
+++ /dev/null
@@ -1,544 +0,0 @@
-/*
- * Copyright (C) 2013 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal.spdy;
-
-import com.squareup.okhttp.Protocol;
-import java.io.IOException;
-import java.util.List;
-import okio.BufferedSink;
-import okio.BufferedSource;
-import okio.ByteString;
-import okio.Deadline;
-import okio.OkBuffer;
-import okio.Source;
-
-/**
- * Read and write http/2 v09 frames.
- * http://tools.ietf.org/html/draft-ietf-httpbis-http2-09
- */
-public final class Http20Draft09 implements Variant {
-
- @Override public Protocol getProtocol() {
- return Protocol.HTTP_2;
- }
-
- private static final ByteString CONNECTION_HEADER
- = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
-
- static final byte TYPE_DATA = 0x0;
- static final byte TYPE_HEADERS = 0x1;
- static final byte TYPE_PRIORITY = 0x2;
- static final byte TYPE_RST_STREAM = 0x3;
- static final byte TYPE_SETTINGS = 0x4;
- static final byte TYPE_PUSH_PROMISE = 0x5;
- static final byte TYPE_PING = 0x6;
- static final byte TYPE_GOAWAY = 0x7;
- static final byte TYPE_WINDOW_UPDATE = 0x9;
- static final byte TYPE_CONTINUATION = 0xa;
-
- static final byte FLAG_NONE = 0x0;
- static final byte FLAG_ACK = 0x1;
- static final byte FLAG_END_STREAM = 0x1;
- static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation.
- static final byte FLAG_END_PUSH_PROMISE = 0x4;
- static final byte FLAG_PRIORITY = 0x8;
-
- @Override public FrameReader newReader(BufferedSource source, boolean client) {
- return new Reader(source, 4096, client);
- }
-
- @Override public FrameWriter newWriter(BufferedSink sink, boolean client) {
- return new Writer(sink, client);
- }
-
- @Override public int maxFrameSize() {
- return 16383;
- }
-
- static final class Reader implements FrameReader {
- private final BufferedSource source;
- private final ContinuationSource continuation;
- private final boolean client;
-
- // Visible for testing.
- final HpackDraft05.Reader hpackReader;
-
- Reader(BufferedSource source, int headerTableSize, boolean client) {
- this.source = source;
- this.client = client;
- this.continuation = new ContinuationSource(this.source);
- this.hpackReader = new HpackDraft05.Reader(client, headerTableSize, continuation);
- }
-
- @Override public void readConnectionHeader() throws IOException {
- if (client) return; // Nothing to read; servers don't send connection headers!
- ByteString connectionHeader = source.readByteString(CONNECTION_HEADER.size());
- if (!CONNECTION_HEADER.equals(connectionHeader)) {
- throw ioException("Expected a connection header but was %s", connectionHeader.utf8());
- }
- }
-
- @Override public boolean nextFrame(Handler handler) throws IOException {
- int w1;
- int w2;
- try {
- w1 = source.readInt();
- w2 = source.readInt();
- } catch (IOException e) {
- return false; // This might be a normal socket close.
- }
-
- // boolean r = (w1 & 0xc0000000) != 0; // Reserved: Ignore first 2 bits.
- short length = (short) ((w1 & 0x3fff0000) >> 16); // 14-bit unsigned == max 16383
- byte type = (byte) ((w1 & 0xff00) >> 8);
- byte flags = (byte) (w1 & 0xff);
- // boolean r = (w2 & 0x80000000) != 0; // Reserved: Ignore first bit.
- int streamId = (w2 & 0x7fffffff); // 31-bit opaque identifier.
-
- switch (type) {
- case TYPE_DATA:
- readData(handler, length, flags, streamId);
- break;
-
- case TYPE_HEADERS:
- readHeaders(handler, length, flags, streamId);
- break;
-
- case TYPE_PRIORITY:
- readPriority(handler, length, flags, streamId);
- break;
-
- case TYPE_RST_STREAM:
- readRstStream(handler, length, flags, streamId);
- break;
-
- case TYPE_SETTINGS:
- readSettings(handler, length, flags, streamId);
- break;
-
- case TYPE_PUSH_PROMISE:
- readPushPromise(handler, length, flags, streamId);
- break;
-
- case TYPE_PING:
- readPing(handler, length, flags, streamId);
- break;
-
- case TYPE_GOAWAY:
- readGoAway(handler, length, flags, streamId);
- break;
-
- case TYPE_WINDOW_UPDATE:
- readWindowUpdate(handler, length, flags, streamId);
- break;
-
- default:
- // Implementations MUST ignore frames of unsupported or unrecognized types.
- source.skip(length);
- }
- return true;
- }
-
- private void readHeaders(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0");
-
- boolean endStream = (flags & FLAG_END_STREAM) != 0;
-
- int priority = -1;
- if ((flags & FLAG_PRIORITY) != 0) {
- priority = source.readInt() & 0x7fffffff;
- length -= 4; // account for above read.
- }
-
- List<Header> headerBlock = readHeaderBlock(length, flags, streamId);
-
- handler.headers(false, endStream, streamId, -1, priority, headerBlock,
- HeadersMode.HTTP_20_HEADERS);
- }
-
- private List<Header> readHeaderBlock(short length, byte flags, int streamId)
- throws IOException {
- continuation.length = continuation.left = length;
- continuation.flags = flags;
- continuation.streamId = streamId;
-
- hpackReader.readHeaders();
- hpackReader.emitReferenceSet();
- // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20.
- // http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3
- return hpackReader.getAndReset();
- }
-
- private void readData(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- boolean inFinished = (flags & FLAG_END_STREAM) != 0;
- // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED
- handler.data(inFinished, streamId, source, length);
- }
-
- private void readPriority(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (length != 4) throw ioException("TYPE_PRIORITY length: %d != 4", length);
- if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0");
- int w1 = source.readInt();
- // boolean r = (w1 & 0x80000000) != 0; // Reserved.
- int priority = (w1 & 0x7fffffff);
- handler.priority(streamId, priority);
- }
-
- private void readRstStream(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length);
- if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0");
- int errorCodeInt = source.readInt();
- ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
- if (errorCode == null) {
- throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
- }
- handler.rstStream(streamId, errorCode);
- }
-
- private void readSettings(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0");
- if ((flags & FLAG_ACK) != 0) {
- if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!");
- handler.ackSettings();
- return;
- }
-
- if (length % 8 != 0) throw ioException("TYPE_SETTINGS length %% 8 != 0: %s", length);
- Settings settings = new Settings();
- for (int i = 0; i < length; i += 8) {
- int w1 = source.readInt();
- int value = source.readInt();
- // int r = (w1 & 0xff000000) >>> 24; // Reserved.
- int id = w1 & 0xffffff;
- settings.set(id, 0, value);
- }
- handler.settings(false, settings);
- if (settings.getHeaderTableSize() >= 0) {
- hpackReader.maxHeaderTableByteCount(settings.getHeaderTableSize());
- }
- }
-
- private void readPushPromise(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (streamId == 0) {
- throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0");
- }
- int promisedStreamId = source.readInt() & 0x7fffffff;
- length -= 4; // account for above read.
- List<Header> headerBlock = readHeaderBlock(length, flags, streamId);
- handler.pushPromise(streamId, promisedStreamId, headerBlock);
- }
-
- private void readPing(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (length != 8) throw ioException("TYPE_PING length != 8: %s", length);
- if (streamId != 0) throw ioException("TYPE_PING streamId != 0");
- int payload1 = source.readInt();
- int payload2 = source.readInt();
- boolean ack = (flags & FLAG_ACK) != 0;
- handler.ping(ack, payload1, payload2);
- }
-
- private void readGoAway(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length);
- if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0");
- int lastStreamId = source.readInt();
- int errorCodeInt = source.readInt();
- int opaqueDataLength = length - 8;
- ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
- if (errorCode == null) {
- throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt);
- }
- ByteString debugData = ByteString.EMPTY;
- if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection.
- debugData = source.readByteString(opaqueDataLength);
- }
- handler.goAway(lastStreamId, errorCode, debugData);
- }
-
- private void readWindowUpdate(Handler handler, short length, byte flags, int streamId)
- throws IOException {
- if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length);
- long increment = (source.readInt() & 0x7fffffff);
- if (increment == 0) throw ioException("windowSizeIncrement was 0", increment);
- handler.windowUpdate(streamId, increment);
- }
-
- @Override public void close() throws IOException {
- source.close();
- }
- }
-
- static final class Writer implements FrameWriter {
- private final BufferedSink sink;
- private final boolean client;
- private final OkBuffer hpackBuffer;
- private final HpackDraft05.Writer hpackWriter;
- private boolean closed;
-
- Writer(BufferedSink sink, boolean client) {
- this.sink = sink;
- this.client = client;
- this.hpackBuffer = new OkBuffer();
- this.hpackWriter = new HpackDraft05.Writer(hpackBuffer);
- }
-
- @Override public synchronized void flush() throws IOException {
- if (closed) throw new IOException("closed");
- sink.flush();
- }
-
- @Override public synchronized void ackSettings() throws IOException {
- if (closed) throw new IOException("closed");
- int length = 0;
- byte type = TYPE_SETTINGS;
- byte flags = FLAG_ACK;
- int streamId = 0;
- frameHeader(length, type, flags, streamId);
- sink.flush();
- }
-
- @Override public synchronized void connectionHeader() throws IOException {
- if (closed) throw new IOException("closed");
- if (!client) return; // Nothing to write; servers don't send connection headers!
- sink.write(CONNECTION_HEADER.toByteArray());
- sink.flush();
- }
-
- @Override public synchronized void synStream(boolean outFinished, boolean inFinished,
- int streamId, int associatedStreamId, int priority, int slot, List<Header> headerBlock)
- throws IOException {
- if (inFinished) throw new UnsupportedOperationException();
- if (closed) throw new IOException("closed");
- headers(outFinished, streamId, priority, headerBlock);
- }
-
- @Override public synchronized void synReply(boolean outFinished, int streamId,
- List<Header> headerBlock) throws IOException {
- if (closed) throw new IOException("closed");
- headers(outFinished, streamId, -1, headerBlock);
- }
-
- @Override public synchronized void headers(int streamId, List<Header> headerBlock)
- throws IOException {
- if (closed) throw new IOException("closed");
- headers(false, streamId, -1, headerBlock);
- }
-
- @Override public synchronized void pushPromise(int streamId, int promisedStreamId,
- List<Header> requestHeaders) throws IOException {
- if (closed) throw new IOException("closed");
- if (hpackBuffer.size() != 0) throw new IllegalStateException();
- hpackWriter.writeHeaders(requestHeaders);
-
- int length = (int) (4 + hpackBuffer.size());
- byte type = TYPE_PUSH_PROMISE;
- byte flags = FLAG_END_HEADERS;
- frameHeader(length, type, flags, streamId); // TODO: CONTINUATION
- sink.writeInt(promisedStreamId & 0x7fffffff);
- sink.write(hpackBuffer, hpackBuffer.size());
- }
-
- private void headers(boolean outFinished, int streamId, int priority,
- List<Header> headerBlock) throws IOException {
- if (closed) throw new IOException("closed");
- if (hpackBuffer.size() != 0) throw new IllegalStateException();
- hpackWriter.writeHeaders(headerBlock);
-
- int length = (int) hpackBuffer.size();
- byte type = TYPE_HEADERS;
- byte flags = FLAG_END_HEADERS;
- if (outFinished) flags |= FLAG_END_STREAM;
- if (priority != -1) flags |= FLAG_PRIORITY;
- if (priority != -1) length += 4;
- frameHeader(length, type, flags, streamId); // TODO: CONTINUATION
- if (priority != -1) sink.writeInt(priority & 0x7fffffff);
- sink.write(hpackBuffer, hpackBuffer.size());
- }
-
- @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
- throws IOException {
- if (closed) throw new IOException("closed");
- if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
-
- int length = 4;
- byte type = TYPE_RST_STREAM;
- byte flags = FLAG_NONE;
- frameHeader(length, type, flags, streamId);
- sink.writeInt(errorCode.httpCode);
- sink.flush();
- }
-
- @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source)
- throws IOException {
- data(outFinished, streamId, source, (int) source.size());
- }
-
- @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source,
- int byteCount) throws IOException {
- if (closed) throw new IOException("closed");
- byte flags = FLAG_NONE;
- if (outFinished) flags |= FLAG_END_STREAM;
- dataFrame(streamId, flags, source, byteCount);
- }
-
- void dataFrame(int streamId, byte flags, OkBuffer buffer, int byteCount) throws IOException {
- byte type = TYPE_DATA;
- frameHeader(byteCount, type, flags, streamId);
- if (byteCount > 0) {
- sink.write(buffer, byteCount);
- }
- }
-
- @Override public synchronized void settings(Settings settings) throws IOException {
- if (closed) throw new IOException("closed");
- int length = settings.size() * 8;
- byte type = TYPE_SETTINGS;
- byte flags = FLAG_NONE;
- int streamId = 0;
- frameHeader(length, type, flags, streamId);
- for (int i = 0; i < Settings.COUNT; i++) {
- if (!settings.isSet(i)) continue;
- sink.writeInt(i & 0xffffff);
- sink.writeInt(settings.get(i));
- }
- sink.flush();
- }
-
- @Override public synchronized void ping(boolean ack, int payload1, int payload2)
- throws IOException {
- if (closed) throw new IOException("closed");
- int length = 8;
- byte type = TYPE_PING;
- byte flags = ack ? FLAG_ACK : FLAG_NONE;
- int streamId = 0;
- frameHeader(length, type, flags, streamId);
- sink.writeInt(payload1);
- sink.writeInt(payload2);
- sink.flush();
- }
-
- @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode,
- byte[] debugData) throws IOException {
- if (closed) throw new IOException("closed");
- if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1");
- int length = 8 + debugData.length;
- byte type = TYPE_GOAWAY;
- byte flags = FLAG_NONE;
- int streamId = 0;
- frameHeader(length, type, flags, streamId);
- sink.writeInt(lastGoodStreamId);
- sink.writeInt(errorCode.httpCode);
- if (debugData.length > 0) {
- sink.write(debugData);
- }
- sink.flush();
- }
-
- @Override public synchronized void windowUpdate(int streamId, long windowSizeIncrement)
- throws IOException {
- if (closed) throw new IOException("closed");
- if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) {
- throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s",
- windowSizeIncrement);
- }
- int length = 4;
- byte type = TYPE_WINDOW_UPDATE;
- byte flags = FLAG_NONE;
- frameHeader(length, type, flags, streamId);
- sink.writeInt((int) windowSizeIncrement);
- sink.flush();
- }
-
- @Override public synchronized void close() throws IOException {
- closed = true;
- sink.close();
- }
-
- void frameHeader(int length, byte type, byte flags, int streamId) throws IOException {
- if (length > 16383) throw illegalArgument("FRAME_SIZE_ERROR length > 16383: %s", length);
- if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId);
- sink.writeInt((length & 0x3fff) << 16 | (type & 0xff) << 8 | (flags & 0xff));
- sink.writeInt(streamId & 0x7fffffff);
- }
- }
-
- private static IllegalArgumentException illegalArgument(String message, Object... args) {
- throw new IllegalArgumentException(String.format(message, args));
- }
-
- private static IOException ioException(String message, Object... args) throws IOException {
- throw new IOException(String.format(message, args));
- }
-
- /**
- * Decompression of the header block occurs above the framing layer. This
- * class lazily reads continuation frames as they are needed by {@link
- * HpackDraft05.Reader#readHeaders()}.
- */
- static final class ContinuationSource implements Source {
- private final BufferedSource source;
-
- int length;
- byte flags;
- int streamId;
-
- int left;
-
- public ContinuationSource(BufferedSource source) {
- this.source = source;
- }
-
- @Override public long read(OkBuffer sink, long byteCount) throws IOException {
- while (left == 0) {
- if ((flags & FLAG_END_HEADERS) != 0) return -1;
- readContinuationHeader();
- // TODO: test case for empty continuation header?
- }
-
- long read = source.read(sink, Math.min(byteCount, left));
- if (read == -1) return -1;
- left -= read;
- return read;
- }
-
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
-
- @Override public void close() throws IOException {
- }
-
- private void readContinuationHeader() throws IOException {
- int previousStreamId = streamId;
- int w1 = source.readInt();
- int w2 = source.readInt();
- length = left = (short) ((w1 & 0x3fff0000) >> 16);
- byte type = (byte) ((w1 & 0xff00) >> 8);
- flags = (byte) (w1 & 0xff);
- streamId = (w2 & 0x7fffffff);
- if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type);
- if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed");
- }
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft16.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft16.java
new file mode 100644
index 0000000..3b6095c
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Http20Draft16.java
@@ -0,0 +1,773 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.spdy;
+
+import com.squareup.okhttp.Protocol;
+import java.io.IOException;
+import java.util.List;
+import java.util.logging.Logger;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.Source;
+import okio.Timeout;
+
+import static com.squareup.okhttp.internal.spdy.Http20Draft16.FrameLogger.formatHeader;
+import static java.lang.String.format;
+import static java.util.logging.Level.FINE;
+import static okio.ByteString.EMPTY;
+
+/**
+ * Read and write HTTP/2 v16 frames.
+ * <p>
+ * This implementation assumes we do not send an increased
+ * {@link Settings#getMaxFrameSize frame size setting} to the peer. Hence, we
+ * expect all frames to have a max length of {@link #INITIAL_MAX_FRAME_SIZE}.
+ * <p>http://tools.ietf.org/html/draft-ietf-httpbis-http2-16
+ */
+public final class Http20Draft16 implements Variant {
+ private static final Logger logger = Logger.getLogger(FrameLogger.class.getName());
+
+ @Override public Protocol getProtocol() {
+ return Protocol.HTTP_2;
+ }
+
+ private static final ByteString CONNECTION_PREFACE
+ = ByteString.encodeUtf8("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n");
+
+ /** The initial max frame size, applied independently writing to, or reading from the peer. */
+ static final int INITIAL_MAX_FRAME_SIZE = 0x4000; // 16384
+
+ static final byte TYPE_DATA = 0x0;
+ static final byte TYPE_HEADERS = 0x1;
+ static final byte TYPE_PRIORITY = 0x2;
+ static final byte TYPE_RST_STREAM = 0x3;
+ static final byte TYPE_SETTINGS = 0x4;
+ static final byte TYPE_PUSH_PROMISE = 0x5;
+ static final byte TYPE_PING = 0x6;
+ static final byte TYPE_GOAWAY = 0x7;
+ static final byte TYPE_WINDOW_UPDATE = 0x8;
+ static final byte TYPE_CONTINUATION = 0x9;
+
+ static final byte FLAG_NONE = 0x0;
+ static final byte FLAG_ACK = 0x1; // Used for settings and ping.
+ static final byte FLAG_END_STREAM = 0x1; // Used for headers and data.
+ static final byte FLAG_END_HEADERS = 0x4; // Used for headers and continuation.
+ static final byte FLAG_END_PUSH_PROMISE = 0x4;
+ static final byte FLAG_PADDED = 0x8; // Used for headers and data.
+ static final byte FLAG_PRIORITY = 0x20; // Used for headers.
+ static final byte FLAG_COMPRESSED = 0x20; // Used for data.
+
+ /**
+ * Creates a frame reader with max header table size of 4096 and data frame
+ * compression disabled.
+ */
+ @Override public FrameReader newReader(BufferedSource source, boolean client) {
+ return new Reader(source, 4096, client);
+ }
+
+ @Override public FrameWriter newWriter(BufferedSink sink, boolean client) {
+ return new Writer(sink, client);
+ }
+
+ static final class Reader implements FrameReader {
+ private final BufferedSource source;
+ private final ContinuationSource continuation;
+ private final boolean client;
+
+ // Visible for testing.
+ final HpackDraft10.Reader hpackReader;
+
+ Reader(BufferedSource source, int headerTableSize, boolean client) {
+ this.source = source;
+ this.client = client;
+ this.continuation = new ContinuationSource(this.source);
+ this.hpackReader = new HpackDraft10.Reader(headerTableSize, continuation);
+ }
+
+ @Override public void readConnectionPreface() throws IOException {
+ if (client) return; // Nothing to read; servers doesn't send a connection preface!
+ ByteString connectionPreface = source.readByteString(CONNECTION_PREFACE.size());
+ if (logger.isLoggable(FINE)) logger.fine(format("<< CONNECTION %s", connectionPreface.hex()));
+ if (!CONNECTION_PREFACE.equals(connectionPreface)) {
+ throw ioException("Expected a connection header but was %s", connectionPreface.utf8());
+ }
+ }
+
+ @Override public boolean nextFrame(Handler handler) throws IOException {
+ try {
+ source.require(9); // Frame header size
+ } catch (IOException e) {
+ return false; // This might be a normal socket close.
+ }
+
+ /* 0 1 2 3
+ * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * | Length (24) |
+ * +---------------+---------------+---------------+
+ * | Type (8) | Flags (8) |
+ * +-+-+-----------+---------------+-------------------------------+
+ * |R| Stream Identifier (31) |
+ * +=+=============================================================+
+ * | Frame Payload (0...) ...
+ * +---------------------------------------------------------------+
+ */
+ int length = readMedium(source);
+ if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
+ throw ioException("FRAME_SIZE_ERROR: %s", length);
+ }
+ byte type = (byte) (source.readByte() & 0xff);
+ byte flags = (byte) (source.readByte() & 0xff);
+ int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
+ if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags));
+
+ switch (type) {
+ case TYPE_DATA:
+ readData(handler, length, flags, streamId);
+ break;
+
+ case TYPE_HEADERS:
+ readHeaders(handler, length, flags, streamId);
+ break;
+
+ case TYPE_PRIORITY:
+ readPriority(handler, length, flags, streamId);
+ break;
+
+ case TYPE_RST_STREAM:
+ readRstStream(handler, length, flags, streamId);
+ break;
+
+ case TYPE_SETTINGS:
+ readSettings(handler, length, flags, streamId);
+ break;
+
+ case TYPE_PUSH_PROMISE:
+ readPushPromise(handler, length, flags, streamId);
+ break;
+
+ case TYPE_PING:
+ readPing(handler, length, flags, streamId);
+ break;
+
+ case TYPE_GOAWAY:
+ readGoAway(handler, length, flags, streamId);
+ break;
+
+ case TYPE_WINDOW_UPDATE:
+ readWindowUpdate(handler, length, flags, streamId);
+ break;
+
+ default:
+ // Implementations MUST discard frames that have unknown or unsupported types.
+ source.skip(length);
+ }
+ return true;
+ }
+
+ private void readHeaders(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0");
+
+ boolean endStream = (flags & FLAG_END_STREAM) != 0;
+
+ short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0;
+
+ if ((flags & FLAG_PRIORITY) != 0) {
+ readPriority(handler, streamId);
+ length -= 5; // account for above read.
+ }
+
+ length = lengthWithoutPadding(length, flags, padding);
+
+ List<Header> headerBlock = readHeaderBlock(length, padding, flags, streamId);
+
+ handler.headers(false, endStream, streamId, -1, headerBlock, HeadersMode.HTTP_20_HEADERS);
+ }
+
+ private List<Header> readHeaderBlock(int length, short padding, byte flags, int streamId)
+ throws IOException {
+ continuation.length = continuation.left = length;
+ continuation.padding = padding;
+ continuation.flags = flags;
+ continuation.streamId = streamId;
+
+ // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20.
+ // http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.5
+ hpackReader.readHeaders();
+ return hpackReader.getAndResetHeaderList();
+ }
+
+ private void readData(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ // TODO: checkState open or half-closed (local) or raise STREAM_CLOSED
+ boolean inFinished = (flags & FLAG_END_STREAM) != 0;
+ boolean gzipped = (flags & FLAG_COMPRESSED) != 0;
+ if (gzipped) {
+ throw ioException("PROTOCOL_ERROR: FLAG_COMPRESSED without SETTINGS_COMPRESS_DATA");
+ }
+
+ short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0;
+ length = lengthWithoutPadding(length, flags, padding);
+
+ handler.data(inFinished, streamId, source, length);
+ source.skip(padding);
+ }
+
+ private void readPriority(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (length != 5) throw ioException("TYPE_PRIORITY length: %d != 5", length);
+ if (streamId == 0) throw ioException("TYPE_PRIORITY streamId == 0");
+ readPriority(handler, streamId);
+ }
+
+ private void readPriority(Handler handler, int streamId) throws IOException {
+ int w1 = source.readInt();
+ boolean exclusive = (w1 & 0x80000000) != 0;
+ int streamDependency = (w1 & 0x7fffffff);
+ int weight = (source.readByte() & 0xff) + 1;
+ handler.priority(streamId, streamDependency, weight, exclusive);
+ }
+
+ private void readRstStream(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (length != 4) throw ioException("TYPE_RST_STREAM length: %d != 4", length);
+ if (streamId == 0) throw ioException("TYPE_RST_STREAM streamId == 0");
+ int errorCodeInt = source.readInt();
+ ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
+ if (errorCode == null) {
+ throw ioException("TYPE_RST_STREAM unexpected error code: %d", errorCodeInt);
+ }
+ handler.rstStream(streamId, errorCode);
+ }
+
+ private void readSettings(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (streamId != 0) throw ioException("TYPE_SETTINGS streamId != 0");
+ if ((flags & FLAG_ACK) != 0) {
+ if (length != 0) throw ioException("FRAME_SIZE_ERROR ack frame should be empty!");
+ handler.ackSettings();
+ return;
+ }
+
+ if (length % 6 != 0) throw ioException("TYPE_SETTINGS length %% 6 != 0: %s", length);
+ Settings settings = new Settings();
+ for (int i = 0; i < length; i += 6) {
+ short id = source.readShort();
+ int value = source.readInt();
+
+ switch (id) {
+ case 1: // SETTINGS_HEADER_TABLE_SIZE
+ break;
+ case 2: // SETTINGS_ENABLE_PUSH
+ if (value != 0 && value != 1) {
+ throw ioException("PROTOCOL_ERROR SETTINGS_ENABLE_PUSH != 0 or 1");
+ }
+ break;
+ case 3: // SETTINGS_MAX_CONCURRENT_STREAMS
+ id = 4; // Renumbered in draft 10.
+ break;
+ case 4: // SETTINGS_INITIAL_WINDOW_SIZE
+ id = 7; // Renumbered in draft 10.
+ if (value < 0) {
+ throw ioException("PROTOCOL_ERROR SETTINGS_INITIAL_WINDOW_SIZE > 2^31 - 1");
+ }
+ break;
+ case 5: // SETTINGS_MAX_FRAME_SIZE
+ if (value < INITIAL_MAX_FRAME_SIZE || value > 16777215) {
+ throw ioException("PROTOCOL_ERROR SETTINGS_MAX_FRAME_SIZE: %s", value);
+ }
+ break;
+ case 6: // SETTINGS_MAX_HEADER_LIST_SIZE
+ break; // Advisory only, so ignored.
+ default:
+ throw ioException("PROTOCOL_ERROR invalid settings id: %s", id);
+ }
+ settings.set(id, 0, value);
+ }
+ handler.settings(false, settings);
+ if (settings.getHeaderTableSize() >= 0) {
+ hpackReader.headerTableSizeSetting(settings.getHeaderTableSize());
+ }
+ }
+
+ private void readPushPromise(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (streamId == 0) {
+ throw ioException("PROTOCOL_ERROR: TYPE_PUSH_PROMISE streamId == 0");
+ }
+ short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0;
+ int promisedStreamId = source.readInt() & 0x7fffffff;
+ length -= 4; // account for above read.
+ length = lengthWithoutPadding(length, flags, padding);
+ List<Header> headerBlock = readHeaderBlock(length, padding, flags, streamId);
+ handler.pushPromise(streamId, promisedStreamId, headerBlock);
+ }
+
+ private void readPing(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (length != 8) throw ioException("TYPE_PING length != 8: %s", length);
+ if (streamId != 0) throw ioException("TYPE_PING streamId != 0");
+ int payload1 = source.readInt();
+ int payload2 = source.readInt();
+ boolean ack = (flags & FLAG_ACK) != 0;
+ handler.ping(ack, payload1, payload2);
+ }
+
+ private void readGoAway(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (length < 8) throw ioException("TYPE_GOAWAY length < 8: %s", length);
+ if (streamId != 0) throw ioException("TYPE_GOAWAY streamId != 0");
+ int lastStreamId = source.readInt();
+ int errorCodeInt = source.readInt();
+ int opaqueDataLength = length - 8;
+ ErrorCode errorCode = ErrorCode.fromHttp2(errorCodeInt);
+ if (errorCode == null) {
+ throw ioException("TYPE_GOAWAY unexpected error code: %d", errorCodeInt);
+ }
+ ByteString debugData = EMPTY;
+ if (opaqueDataLength > 0) { // Must read debug data in order to not corrupt the connection.
+ debugData = source.readByteString(opaqueDataLength);
+ }
+ handler.goAway(lastStreamId, errorCode, debugData);
+ }
+
+ private void readWindowUpdate(Handler handler, int length, byte flags, int streamId)
+ throws IOException {
+ if (length != 4) throw ioException("TYPE_WINDOW_UPDATE length !=4: %s", length);
+ long increment = (source.readInt() & 0x7fffffffL);
+ if (increment == 0) throw ioException("windowSizeIncrement was 0", increment);
+ handler.windowUpdate(streamId, increment);
+ }
+
+ @Override public void close() throws IOException {
+ source.close();
+ }
+ }
+
+ static final class Writer implements FrameWriter {
+ private final BufferedSink sink;
+ private final boolean client;
+ private final Buffer hpackBuffer;
+ private final HpackDraft10.Writer hpackWriter;
+ private int maxFrameSize;
+ private boolean closed;
+
+ Writer(BufferedSink sink, boolean client) {
+ this.sink = sink;
+ this.client = client;
+ this.hpackBuffer = new Buffer();
+ this.hpackWriter = new HpackDraft10.Writer(hpackBuffer);
+ this.maxFrameSize = INITIAL_MAX_FRAME_SIZE;
+ }
+
+ @Override public synchronized void flush() throws IOException {
+ if (closed) throw new IOException("closed");
+ sink.flush();
+ }
+
+ @Override public synchronized void ackSettings(Settings peerSettings) throws IOException {
+ if (closed) throw new IOException("closed");
+ this.maxFrameSize = peerSettings.getMaxFrameSize(maxFrameSize);
+ int length = 0;
+ byte type = TYPE_SETTINGS;
+ byte flags = FLAG_ACK;
+ int streamId = 0;
+ frameHeader(streamId, length, type, flags);
+ sink.flush();
+ }
+
+ @Override public synchronized void connectionPreface() throws IOException {
+ if (closed) throw new IOException("closed");
+ if (!client) return; // Nothing to write; servers don't send connection headers!
+ if (logger.isLoggable(FINE)) {
+ logger.fine(format(">> CONNECTION %s", CONNECTION_PREFACE.hex()));
+ }
+ sink.write(CONNECTION_PREFACE.toByteArray());
+ sink.flush();
+ }
+
+ @Override public synchronized void synStream(boolean outFinished, boolean inFinished,
+ int streamId, int associatedStreamId, List<Header> headerBlock)
+ throws IOException {
+ if (inFinished) throw new UnsupportedOperationException();
+ if (closed) throw new IOException("closed");
+ headers(outFinished, streamId, headerBlock);
+ }
+
+ @Override public synchronized void synReply(boolean outFinished, int streamId,
+ List<Header> headerBlock) throws IOException {
+ if (closed) throw new IOException("closed");
+ headers(outFinished, streamId, headerBlock);
+ }
+
+ @Override public synchronized void headers(int streamId, List<Header> headerBlock)
+ throws IOException {
+ if (closed) throw new IOException("closed");
+ headers(false, streamId, headerBlock);
+ }
+
+ @Override public synchronized void pushPromise(int streamId, int promisedStreamId,
+ List<Header> requestHeaders) throws IOException {
+ if (closed) throw new IOException("closed");
+ if (hpackBuffer.size() != 0) throw new IllegalStateException();
+ hpackWriter.writeHeaders(requestHeaders);
+
+ long byteCount = hpackBuffer.size();
+ int length = (int) Math.min(maxFrameSize - 4, byteCount);
+ byte type = TYPE_PUSH_PROMISE;
+ byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
+ frameHeader(streamId, length + 4, type, flags);
+ sink.writeInt(promisedStreamId & 0x7fffffff);
+ sink.write(hpackBuffer, length);
+
+ if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
+ }
+
+ void headers(boolean outFinished, int streamId, List<Header> headerBlock) throws IOException {
+ if (closed) throw new IOException("closed");
+ if (hpackBuffer.size() != 0) throw new IllegalStateException();
+ hpackWriter.writeHeaders(headerBlock);
+
+ long byteCount = hpackBuffer.size();
+ int length = (int) Math.min(maxFrameSize, byteCount);
+ byte type = TYPE_HEADERS;
+ byte flags = byteCount == length ? FLAG_END_HEADERS : 0;
+ if (outFinished) flags |= FLAG_END_STREAM;
+ frameHeader(streamId, length, type, flags);
+ sink.write(hpackBuffer, length);
+
+ if (byteCount > length) writeContinuationFrames(streamId, byteCount - length);
+ }
+
+ private void writeContinuationFrames(int streamId, long byteCount) throws IOException {
+ while (byteCount > 0) {
+ int length = (int) Math.min(maxFrameSize, byteCount);
+ byteCount -= length;
+ frameHeader(streamId, length, TYPE_CONTINUATION, byteCount == 0 ? FLAG_END_HEADERS : 0);
+ sink.write(hpackBuffer, length);
+ }
+ }
+
+ @Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
+ throws IOException {
+ if (closed) throw new IOException("closed");
+ if (errorCode.spdyRstCode == -1) throw new IllegalArgumentException();
+
+ int length = 4;
+ byte type = TYPE_RST_STREAM;
+ byte flags = FLAG_NONE;
+ frameHeader(streamId, length, type, flags);
+ sink.writeInt(errorCode.httpCode);
+ sink.flush();
+ }
+
+ @Override public int maxDataLength() {
+ return maxFrameSize;
+ }
+
+ @Override public synchronized void data(boolean outFinished, int streamId, Buffer source,
+ int byteCount) throws IOException {
+ if (closed) throw new IOException("closed");
+ byte flags = FLAG_NONE;
+ if (outFinished) flags |= FLAG_END_STREAM;
+ dataFrame(streamId, flags, source, byteCount);
+ }
+
+ void dataFrame(int streamId, byte flags, Buffer buffer, int byteCount) throws IOException {
+ byte type = TYPE_DATA;
+ frameHeader(streamId, byteCount, type, flags);
+ if (byteCount > 0) {
+ sink.write(buffer, byteCount);
+ }
+ }
+
+ @Override public synchronized void settings(Settings settings) throws IOException {
+ if (closed) throw new IOException("closed");
+ int length = settings.size() * 6;
+ byte type = TYPE_SETTINGS;
+ byte flags = FLAG_NONE;
+ int streamId = 0;
+ frameHeader(streamId, length, type, flags);
+ for (int i = 0; i < Settings.COUNT; i++) {
+ if (!settings.isSet(i)) continue;
+ int id = i;
+ if (id == 4) id = 3; // SETTINGS_MAX_CONCURRENT_STREAMS renumbered.
+ else if (id == 7) id = 4; // SETTINGS_INITIAL_WINDOW_SIZE renumbered.
+ sink.writeShort(id);
+ sink.writeInt(settings.get(i));
+ }
+ sink.flush();
+ }
+
+ @Override public synchronized void ping(boolean ack, int payload1, int payload2)
+ throws IOException {
+ if (closed) throw new IOException("closed");
+ int length = 8;
+ byte type = TYPE_PING;
+ byte flags = ack ? FLAG_ACK : FLAG_NONE;
+ int streamId = 0;
+ frameHeader(streamId, length, type, flags);
+ sink.writeInt(payload1);
+ sink.writeInt(payload2);
+ sink.flush();
+ }
+
+ @Override public synchronized void goAway(int lastGoodStreamId, ErrorCode errorCode,
+ byte[] debugData) throws IOException {
+ if (closed) throw new IOException("closed");
+ if (errorCode.httpCode == -1) throw illegalArgument("errorCode.httpCode == -1");
+ int length = 8 + debugData.length;
+ byte type = TYPE_GOAWAY;
+ byte flags = FLAG_NONE;
+ int streamId = 0;
+ frameHeader(streamId, length, type, flags);
+ sink.writeInt(lastGoodStreamId);
+ sink.writeInt(errorCode.httpCode);
+ if (debugData.length > 0) {
+ sink.write(debugData);
+ }
+ sink.flush();
+ }
+
+ @Override public synchronized void windowUpdate(int streamId, long windowSizeIncrement)
+ throws IOException {
+ if (closed) throw new IOException("closed");
+ if (windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL) {
+ throw illegalArgument("windowSizeIncrement == 0 || windowSizeIncrement > 0x7fffffffL: %s",
+ windowSizeIncrement);
+ }
+ int length = 4;
+ byte type = TYPE_WINDOW_UPDATE;
+ byte flags = FLAG_NONE;
+ frameHeader(streamId, length, type, flags);
+ sink.writeInt((int) windowSizeIncrement);
+ sink.flush();
+ }
+
+ @Override public synchronized void close() throws IOException {
+ closed = true;
+ sink.close();
+ }
+
+ void frameHeader(int streamId, int length, byte type, byte flags) throws IOException {
+ if (logger.isLoggable(FINE)) logger.fine(formatHeader(false, streamId, length, type, flags));
+ if (length > maxFrameSize) {
+ throw illegalArgument("FRAME_SIZE_ERROR length > %d: %d", maxFrameSize, length);
+ }
+ if ((streamId & 0x80000000) != 0) throw illegalArgument("reserved bit set: %s", streamId);
+ writeMedium(sink, length);
+ sink.writeByte(type & 0xff);
+ sink.writeByte(flags & 0xff);
+ sink.writeInt(streamId & 0x7fffffff);
+ }
+ }
+
+ private static IllegalArgumentException illegalArgument(String message, Object... args) {
+ throw new IllegalArgumentException(format(message, args));
+ }
+
+ private static IOException ioException(String message, Object... args) throws IOException {
+ throw new IOException(format(message, args));
+ }
+
+ /**
+ * Decompression of the header block occurs above the framing layer. This
+ * class lazily reads continuation frames as they are needed by {@link
+ * HpackDraft10.Reader#readHeaders()}.
+ */
+ static final class ContinuationSource implements Source {
+ private final BufferedSource source;
+
+ int length;
+ byte flags;
+ int streamId;
+
+ int left;
+ short padding;
+
+ public ContinuationSource(BufferedSource source) {
+ this.source = source;
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ while (left == 0) {
+ source.skip(padding);
+ padding = 0;
+ if ((flags & FLAG_END_HEADERS) != 0) return -1;
+ readContinuationHeader();
+ // TODO: test case for empty continuation header?
+ }
+
+ long read = source.read(sink, Math.min(byteCount, left));
+ if (read == -1) return -1;
+ left -= read;
+ return read;
+ }
+
+ @Override public Timeout timeout() {
+ return source.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ }
+
+ private void readContinuationHeader() throws IOException {
+ int previousStreamId = streamId;
+
+ length = left = readMedium(source);
+ byte type = (byte) (source.readByte() & 0xff);
+ flags = (byte) (source.readByte() & 0xff);
+ if (logger.isLoggable(FINE)) logger.fine(formatHeader(true, streamId, length, type, flags));
+ streamId = (source.readInt() & 0x7fffffff);
+ if (type != TYPE_CONTINUATION) throw ioException("%s != TYPE_CONTINUATION", type);
+ if (streamId != previousStreamId) throw ioException("TYPE_CONTINUATION streamId changed");
+ }
+ }
+
+ private static int lengthWithoutPadding(int length, byte flags, short padding)
+ throws IOException {
+ if ((flags & FLAG_PADDED) != 0) length--; // Account for reading the padding length.
+ if (padding > length) {
+ throw ioException("PROTOCOL_ERROR padding %s > remaining length %s", padding, length);
+ }
+ return (short) (length - padding);
+ }
+
+ /**
+ * Logs a human-readable representation of HTTP/2 frame headers.
+ *
+ * <p>The format is:
+ *
+ * <pre>
+ * direction streamID length type flags
+ * </pre>
+ * Where direction is {@code <<} for inbound and {@code >>} for outbound.
+ *
+ * <p> For example, the following would indicate a HEAD request sent from
+ * the client.
+ * <pre>
+ * {@code
+ * << 0x0000000f 12 HEADERS END_HEADERS|END_STREAM
+ * }
+ * </pre>
+ */
+ static final class FrameLogger {
+
+ static String formatHeader(boolean inbound, int streamId, int length, byte type, byte flags) {
+ String formattedType = type < TYPES.length ? TYPES[type] : format("0x%02x", type);
+ String formattedFlags = formatFlags(type, flags);
+ return format("%s 0x%08x %5d %-13s %s", inbound ? "<<" : ">>", streamId, length,
+ formattedType, formattedFlags);
+ }
+
+ /**
+ * Looks up valid string representing flags from the table. Invalid
+ * combinations are represented in binary.
+ */
+ // Visible for testing.
+ static String formatFlags(byte type, byte flags) {
+ if (flags == 0) return "";
+ switch (type) { // Special case types that have 0 or 1 flag.
+ case TYPE_SETTINGS:
+ case TYPE_PING:
+ return flags == FLAG_ACK ? "ACK" : BINARY[flags];
+ case TYPE_PRIORITY:
+ case TYPE_RST_STREAM:
+ case TYPE_GOAWAY:
+ case TYPE_WINDOW_UPDATE:
+ return BINARY[flags];
+ }
+ String result = flags < FLAGS.length ? FLAGS[flags] : BINARY[flags];
+ // Special case types that have overlap flag values.
+ if (type == TYPE_PUSH_PROMISE && (flags & FLAG_END_PUSH_PROMISE) != 0) {
+ return result.replace("HEADERS", "PUSH_PROMISE"); // TODO: Avoid allocation.
+ } else if (type == TYPE_DATA && (flags & FLAG_COMPRESSED) != 0) {
+ return result.replace("PRIORITY", "COMPRESSED"); // TODO: Avoid allocation.
+ }
+ return result;
+ }
+
+ /** Lookup table for valid frame types. */
+ private static final String[] TYPES = new String[] {
+ "DATA",
+ "HEADERS",
+ "PRIORITY",
+ "RST_STREAM",
+ "SETTINGS",
+ "PUSH_PROMISE",
+ "PING",
+ "GOAWAY",
+ "WINDOW_UPDATE",
+ "CONTINUATION"
+ };
+
+ /**
+ * Lookup table for valid flags for DATA, HEADERS, CONTINUATION. Invalid
+ * combinations are represented in binary.
+ */
+ private static final String[] FLAGS = new String[0x40]; // Highest bit flag is 0x20.
+ private static final String[] BINARY = new String[256];
+
+ static {
+ for (int i = 0; i < BINARY.length; i++) {
+ BINARY[i] = format("%8s", Integer.toBinaryString(i)).replace(' ', '0');
+ }
+
+ FLAGS[FLAG_NONE] = "";
+ FLAGS[FLAG_END_STREAM] = "END_STREAM";
+
+ int[] prefixFlags = new int[] {FLAG_END_STREAM};
+
+ FLAGS[FLAG_PADDED] = "PADDED";
+ for (int prefixFlag : prefixFlags) {
+ FLAGS[prefixFlag | FLAG_PADDED] = FLAGS[prefixFlag] + "|PADDED";
+ }
+
+ FLAGS[FLAG_END_HEADERS] = "END_HEADERS"; // Same as END_PUSH_PROMISE.
+ FLAGS[FLAG_PRIORITY] = "PRIORITY"; // Same as FLAG_COMPRESSED.
+ FLAGS[FLAG_END_HEADERS | FLAG_PRIORITY] = "END_HEADERS|PRIORITY"; // Only valid on HEADERS.
+ int[] frameFlags =
+ new int[] {FLAG_END_HEADERS, FLAG_PRIORITY, FLAG_END_HEADERS | FLAG_PRIORITY};
+
+ for (int frameFlag : frameFlags) {
+ for (int prefixFlag : prefixFlags) {
+ FLAGS[prefixFlag | frameFlag] = FLAGS[prefixFlag] + '|' + FLAGS[frameFlag];
+ FLAGS[prefixFlag | frameFlag | FLAG_PADDED] =
+ FLAGS[prefixFlag] + '|' + FLAGS[frameFlag] + "|PADDED";
+ }
+ }
+
+ for (int i = 0; i < FLAGS.length; i++) { // Fill in holes with binary representation.
+ if (FLAGS[i] == null) FLAGS[i] = BINARY[i];
+ }
+ }
+ }
+
+ private static int readMedium(BufferedSource source) throws IOException {
+ return (source.readByte() & 0xff) << 16
+ | (source.readByte() & 0xff) << 8
+ | (source.readByte() & 0xff);
+ }
+
+ private static void writeMedium(BufferedSink sink, int i) throws IOException {
+ sink.writeByte((i >>> 16) & 0xff);
+ sink.writeByte((i >>> 8) & 0xff);
+ sink.writeByte(i & 0xff);
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
index 45d882f..298087b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Huffman.java
@@ -18,7 +18,6 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
-import okio.ByteString;
/**
* This class was originally composed from the following classes in
@@ -30,132 +29,165 @@
* </ul>
*/
class Huffman {
- enum Codec {
- REQUEST(REQUEST_CODES, REQUEST_CODE_LENGTHS),
- RESPONSE(RESPONSE_CODES, RESPONSE_CODE_LENGTHS);
- private final Node root = new Node();
- private final int[] codes;
- private final byte[] lengths;
+ // Appendix C: Huffman Codes
+ // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-10#appendix-B
+ private static final int[] CODES = {
+ 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7, 0xfffffe8,
+ 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec, 0xfffffed,
+ 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3, 0xffffff4,
+ 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb, 0x14, 0x3f8,
+ 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa, 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18,
+ 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb,
+ 0x3fc, 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
+ 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0,
+ 0x1ffc, 0x3ffc, 0x22, 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26, 0x27, 0x6, 0x74, 0x75,
+ 0x28, 0x29, 0x2a, 0x7, 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7ffe,
+ 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc, 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4,
+ 0x3fffd5, 0x7fffd9, 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb,
+ 0x7fffdf, 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3,
+ 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef, 0x3fffda,
+ 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde, 0x7fffea, 0x3fffdd,
+ 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec, 0x1fffe0, 0x1fffe1, 0x3fffe0,
+ 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef, 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4,
+ 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1, 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7,
+ 0x7ffff2, 0x3fffe8, 0x1ffffec, 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf,
+ 0x3ffffe5, 0xfffff1, 0x1ffffed, 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7,
+ 0x7ffffe2, 0xfffff2, 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3,
+ 0x7ffffe4, 0x7ffffe5, 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8,
+ 0x7ffff3, 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4,
+ 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea,
+ 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee
+ };
- /**
- * @param codes Index designates the symbol this code represents.
- * @param lengths Index designates the symbol this code represents.
- */
- Codec(int[] codes, byte[] lengths) {
- buildTree(codes, lengths);
- this.codes = codes;
- this.lengths = lengths;
- }
+ private static final byte[] CODE_LENGTHS = {
+ 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 30,
+ 28, 28, 28, 28, 28, 28, 28, 28, 28, 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6, 5,
+ 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10, 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
+ 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6, 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6,
+ 6, 6, 5, 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28, 20, 22, 20, 20, 22, 22, 22, 23,
+ 22, 23, 23, 23, 23, 23, 24, 23, 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23,
+ 24, 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23, 21, 21, 22, 21, 23, 22,
+ 23, 23, 20, 22, 22, 22, 23, 22, 22, 23, 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27,
+ 26, 24, 25, 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27, 20, 24, 20, 21,
+ 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23, 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27,
+ 27, 27, 27, 27, 26
+ };
- void encode(byte[] data, OutputStream out) throws IOException {
- long current = 0;
- int n = 0;
+ private static final Huffman INSTANCE = new Huffman();
- for (int i = 0; i < data.length; i++) {
- int b = data[i] & 0xFF;
- int code = codes[b];
- int nbits = lengths[b];
+ public static Huffman get() {
+ return INSTANCE;
+ }
- current <<= nbits;
- current |= code;
- n += nbits;
+ private final Node root = new Node();
- while (n >= 8) {
- n -= 8;
- out.write(((int) (current >> n)));
- }
- }
+ private Huffman() {
+ buildTree();
+ }
- if (n > 0) {
- current <<= (8 - n);
- current |= (0xFF >>> n);
- out.write((int) current);
+ void encode(byte[] data, OutputStream out) throws IOException {
+ long current = 0;
+ int n = 0;
+
+ for (int i = 0; i < data.length; i++) {
+ int b = data[i] & 0xFF;
+ int code = CODES[b];
+ int nbits = CODE_LENGTHS[b];
+
+ current <<= nbits;
+ current |= code;
+ n += nbits;
+
+ while (n >= 8) {
+ n -= 8;
+ out.write(((int) (current >> n)));
}
}
- int encodedLength(byte[] bytes) {
- long len = 0;
+ if (n > 0) {
+ current <<= (8 - n);
+ current |= (0xFF >>> n);
+ out.write((int) current);
+ }
+ }
- for (int i = 0; i < bytes.length; i++) {
- int b = bytes[i] & 0xFF;
- len += lengths[b];
- }
+ int encodedLength(byte[] bytes) {
+ long len = 0;
- return (int) ((len + 7) >> 3);
+ for (int i = 0; i < bytes.length; i++) {
+ int b = bytes[i] & 0xFF;
+ len += CODE_LENGTHS[b];
}
- ByteString decode(ByteString buf) throws IOException {
- return ByteString.of(decode(buf.toByteArray()));
- }
+ return (int) ((len + 7) >> 3);
+ }
- byte[] decode(byte[] buf) throws IOException {
- // FIXME
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- Node node = root;
- int current = 0;
- int nbits = 0;
- for (int i = 0; i < buf.length; i++) {
- int b = buf[i] & 0xFF;
- current = (current << 8) | b;
- nbits += 8;
- while (nbits >= 8) {
- int c = (current >>> (nbits - 8)) & 0xFF;
- node = node.children[c];
- if (node.children == null) {
- // terminal node
- baos.write(node.symbol);
- nbits -= node.terminalBits;
- node = root;
- } else {
- // non-terminal node
- nbits -= 8;
- }
- }
- }
-
- while (nbits > 0) {
- int c = (current << (8 - nbits)) & 0xFF;
+ byte[] decode(byte[] buf) throws IOException {
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ Node node = root;
+ int current = 0;
+ int nbits = 0;
+ for (int i = 0; i < buf.length; i++) {
+ int b = buf[i] & 0xFF;
+ current = (current << 8) | b;
+ nbits += 8;
+ while (nbits >= 8) {
+ int c = (current >>> (nbits - 8)) & 0xFF;
node = node.children[c];
- if (node.children != null || node.terminalBits > nbits) {
- break;
+ if (node.children == null) {
+ // terminal node
+ baos.write(node.symbol);
+ nbits -= node.terminalBits;
+ node = root;
+ } else {
+ // non-terminal node
+ nbits -= 8;
}
- baos.write(node.symbol);
- nbits -= node.terminalBits;
- node = root;
- }
-
- return baos.toByteArray();
- }
-
- private void buildTree(int[] codes, byte[] lengths) {
- for (int i = 0; i < lengths.length; i++) {
- addCode(i, codes[i], lengths[i]);
}
}
- private void addCode(int sym, int code, byte len) {
- Node terminal = new Node(sym, len);
-
- Node current = root;
- while (len > 8) {
- len -= 8;
- int i = ((code >>> len) & 0xFF);
- if (current.children == null) {
- throw new IllegalStateException("invalid dictionary: prefix not unique");
- }
- if (current.children[i] == null) {
- current.children[i] = new Node();
- }
- current = current.children[i];
+ while (nbits > 0) {
+ int c = (current << (8 - nbits)) & 0xFF;
+ node = node.children[c];
+ if (node.children != null || node.terminalBits > nbits) {
+ break;
}
+ baos.write(node.symbol);
+ nbits -= node.terminalBits;
+ node = root;
+ }
- int shift = 8 - len;
- int start = (code << shift) & 0xFF;
- int end = 1 << shift;
- for (int i = start; i < start + end; i++) {
- current.children[i] = terminal;
+ return baos.toByteArray();
+ }
+
+ private void buildTree() {
+ for (int i = 0; i < CODE_LENGTHS.length; i++) {
+ addCode(i, CODES[i], CODE_LENGTHS[i]);
+ }
+ }
+
+ private void addCode(int sym, int code, byte len) {
+ Node terminal = new Node(sym, len);
+
+ Node current = root;
+ while (len > 8) {
+ len -= 8;
+ int i = ((code >>> len) & 0xFF);
+ if (current.children == null) {
+ throw new IllegalStateException("invalid dictionary: prefix not unique");
}
+ if (current.children[i] == null) {
+ current.children[i] = new Node();
+ }
+ current = current.children[i];
+ }
+
+ int shift = 8 - len;
+ int start = (code << shift) & 0xFF;
+ int end = 1 << shift;
+ for (int i = start; i < start + end; i++) {
+ current.children[i] = terminal;
}
}
@@ -190,95 +222,4 @@
this.terminalBits = b == 0 ? 8 : b;
}
}
-
- // Appendix C: Huffman Codes For Requests
- // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-C
- private static final int[] REQUEST_CODES = {
- 0x7ffffba, 0x7ffffbb, 0x7ffffbc, 0x7ffffbd, 0x7ffffbe, 0x7ffffbf, 0x7ffffc0, 0x7ffffc1,
- 0x7ffffc2, 0x7ffffc3, 0x7ffffc4, 0x7ffffc5, 0x7ffffc6, 0x7ffffc7, 0x7ffffc8, 0x7ffffc9,
- 0x7ffffca, 0x7ffffcb, 0x7ffffcc, 0x7ffffcd, 0x7ffffce, 0x7ffffcf, 0x7ffffd0, 0x7ffffd1,
- 0x7ffffd2, 0x7ffffd3, 0x7ffffd4, 0x7ffffd5, 0x7ffffd6, 0x7ffffd7, 0x7ffffd8, 0x7ffffd9, 0xe8,
- 0xffc, 0x3ffa, 0x7ffc, 0x7ffd, 0x24, 0x6e, 0x7ffe, 0x7fa, 0x7fb, 0x3fa, 0x7fc, 0xe9, 0x25,
- 0x4, 0x0, 0x5, 0x6, 0x7, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x1ec, 0xea, 0x3fffe, 0x2d,
- 0x1fffc, 0x1ed, 0x3ffb, 0x6f, 0xeb, 0xec, 0xed, 0xee, 0x70, 0x1ee, 0x1ef, 0x1f0, 0x1f1, 0x3fb,
- 0x1f2, 0xef, 0x1f3, 0x1f4, 0x1f5, 0x1f6, 0x1f7, 0xf0, 0xf1, 0x1f8, 0x1f9, 0x1fa, 0x1fb, 0x1fc,
- 0x3fc, 0x3ffc, 0x7ffffda, 0x1ffc, 0x3ffd, 0x2e, 0x7fffe, 0x8, 0x2f, 0x9, 0x30, 0x1, 0x31,
- 0x32, 0x33, 0xa, 0x71, 0x72, 0xb, 0x34, 0xc, 0xd, 0xe, 0xf2, 0xf, 0x10, 0x11, 0x35, 0x73,
- 0x36, 0xf3, 0xf4, 0xf5, 0x1fffd, 0x7fd, 0x1fffe, 0xffd, 0x7ffffdb, 0x7ffffdc, 0x7ffffdd,
- 0x7ffffde, 0x7ffffdf, 0x7ffffe0, 0x7ffffe1, 0x7ffffe2, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5,
- 0x7ffffe6, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea, 0x7ffffeb, 0x7ffffec, 0x7ffffed,
- 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x7fffff1, 0x7fffff2, 0x7fffff3, 0x7fffff4, 0x7fffff5,
- 0x7fffff6, 0x7fffff7, 0x7fffff8, 0x7fffff9, 0x7fffffa, 0x7fffffb, 0x7fffffc, 0x7fffffd,
- 0x7fffffe, 0x7ffffff, 0x3ffff80, 0x3ffff81, 0x3ffff82, 0x3ffff83, 0x3ffff84, 0x3ffff85,
- 0x3ffff86, 0x3ffff87, 0x3ffff88, 0x3ffff89, 0x3ffff8a, 0x3ffff8b, 0x3ffff8c, 0x3ffff8d,
- 0x3ffff8e, 0x3ffff8f, 0x3ffff90, 0x3ffff91, 0x3ffff92, 0x3ffff93, 0x3ffff94, 0x3ffff95,
- 0x3ffff96, 0x3ffff97, 0x3ffff98, 0x3ffff99, 0x3ffff9a, 0x3ffff9b, 0x3ffff9c, 0x3ffff9d,
- 0x3ffff9e, 0x3ffff9f, 0x3ffffa0, 0x3ffffa1, 0x3ffffa2, 0x3ffffa3, 0x3ffffa4, 0x3ffffa5,
- 0x3ffffa6, 0x3ffffa7, 0x3ffffa8, 0x3ffffa9, 0x3ffffaa, 0x3ffffab, 0x3ffffac, 0x3ffffad,
- 0x3ffffae, 0x3ffffaf, 0x3ffffb0, 0x3ffffb1, 0x3ffffb2, 0x3ffffb3, 0x3ffffb4, 0x3ffffb5,
- 0x3ffffb6, 0x3ffffb7, 0x3ffffb8, 0x3ffffb9, 0x3ffffba, 0x3ffffbb, 0x3ffffbc, 0x3ffffbd,
- 0x3ffffbe, 0x3ffffbf, 0x3ffffc0, 0x3ffffc1, 0x3ffffc2, 0x3ffffc3, 0x3ffffc4, 0x3ffffc5,
- 0x3ffffc6, 0x3ffffc7, 0x3ffffc8, 0x3ffffc9, 0x3ffffca, 0x3ffffcb, 0x3ffffcc, 0x3ffffcd,
- 0x3ffffce, 0x3ffffcf, 0x3ffffd0, 0x3ffffd1, 0x3ffffd2, 0x3ffffd3, 0x3ffffd4, 0x3ffffd5,
- 0x3ffffd6, 0x3ffffd7, 0x3ffffd8, 0x3ffffd9, 0x3ffffda, 0x3ffffdb
- };
-
- private static final byte[] REQUEST_CODE_LENGTHS = {
- 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
- 27, 27, 27, 27, 27, 27, 27, 27, 27, 8, 12, 14, 15, 15, 6, 7, 15, 11, 11, 10, 11, 8, 6, 5, 4,
- 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 9, 8, 18, 6, 17, 9, 14, 7, 8, 8, 8, 8, 7, 9, 9, 9, 9, 10, 9, 8,
- 9, 9, 9, 9, 9, 8, 8, 9, 9, 9, 9, 9, 10, 14, 27, 13, 14, 6, 19, 5, 6, 5, 6, 4, 6, 6, 6, 5, 7,
- 7, 5, 6, 5, 5, 5, 8, 5, 5, 5, 6, 7, 6, 8, 8, 8, 17, 11, 17, 12, 27, 27, 27, 27, 27, 27, 27,
- 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27, 27,
- 27, 27, 27, 27, 27, 27, 27, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
- 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
- 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
- 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26, 26,
- 26, 26, 26, 26, 26, 26, 26
- };
-
- // Appendix D: Huffman Codes For Responses
- // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-D
- private static final int[] RESPONSE_CODES = {
- 0x1ffffbc, 0x1ffffbd, 0x1ffffbe, 0x1ffffbf, 0x1ffffc0, 0x1ffffc1, 0x1ffffc2, 0x1ffffc3,
- 0x1ffffc4, 0x1ffffc5, 0x1ffffc6, 0x1ffffc7, 0x1ffffc8, 0x1ffffc9, 0x1ffffca, 0x1ffffcb,
- 0x1ffffcc, 0x1ffffcd, 0x1ffffce, 0x1ffffcf, 0x1ffffd0, 0x1ffffd1, 0x1ffffd2, 0x1ffffd3,
- 0x1ffffd4, 0x1ffffd5, 0x1ffffd6, 0x1ffffd7, 0x1ffffd8, 0x1ffffd9, 0x1ffffda, 0x1ffffdb, 0x0,
- 0xffa, 0x6a, 0x1ffa, 0x3ffc, 0x1ec, 0x3f8, 0x1ffb, 0x1ed, 0x1ee, 0xffb, 0x7fa, 0x22, 0x23,
- 0x24, 0x6b, 0x1, 0x2, 0x3, 0x8, 0x9, 0xa, 0x25, 0x26, 0xb, 0xc, 0xd, 0x1ef, 0xfffa, 0x6c,
- 0x1ffc, 0xffc, 0xfffb, 0x6d, 0xea, 0xeb, 0xec, 0xed, 0xee, 0x27, 0x1f0, 0xef, 0xf0, 0x3f9,
- 0x1f1, 0x28, 0xf1, 0xf2, 0x1f2, 0x3fa, 0x1f3, 0x29, 0xe, 0x1f4, 0x1f5, 0xf3, 0x3fb, 0x1f6,
- 0x3fc, 0x7fb, 0x1ffd, 0x7fc, 0x7ffc, 0x1f7, 0x1fffe, 0xf, 0x6e, 0x2a, 0x2b, 0x10, 0x6f, 0x70,
- 0x71, 0x2c, 0x1f8, 0x1f9, 0x72, 0x2d, 0x2e, 0x2f, 0x30, 0x1fa, 0x31, 0x32, 0x33, 0x34, 0x73,
- 0xf4, 0x74, 0xf5, 0x1fb, 0xfffc, 0x3ffd, 0xfffd, 0xfffe, 0x1ffffdc, 0x1ffffdd, 0x1ffffde,
- 0x1ffffdf, 0x1ffffe0, 0x1ffffe1, 0x1ffffe2, 0x1ffffe3, 0x1ffffe4, 0x1ffffe5, 0x1ffffe6,
- 0x1ffffe7, 0x1ffffe8, 0x1ffffe9, 0x1ffffea, 0x1ffffeb, 0x1ffffec, 0x1ffffed, 0x1ffffee,
- 0x1ffffef, 0x1fffff0, 0x1fffff1, 0x1fffff2, 0x1fffff3, 0x1fffff4, 0x1fffff5, 0x1fffff6,
- 0x1fffff7, 0x1fffff8, 0x1fffff9, 0x1fffffa, 0x1fffffb, 0x1fffffc, 0x1fffffd, 0x1fffffe,
- 0x1ffffff, 0xffff80, 0xffff81, 0xffff82, 0xffff83, 0xffff84, 0xffff85, 0xffff86, 0xffff87,
- 0xffff88, 0xffff89, 0xffff8a, 0xffff8b, 0xffff8c, 0xffff8d, 0xffff8e, 0xffff8f, 0xffff90,
- 0xffff91, 0xffff92, 0xffff93, 0xffff94, 0xffff95, 0xffff96, 0xffff97, 0xffff98, 0xffff99,
- 0xffff9a, 0xffff9b, 0xffff9c, 0xffff9d, 0xffff9e, 0xffff9f, 0xffffa0, 0xffffa1, 0xffffa2,
- 0xffffa3, 0xffffa4, 0xffffa5, 0xffffa6, 0xffffa7, 0xffffa8, 0xffffa9, 0xffffaa, 0xffffab,
- 0xffffac, 0xffffad, 0xffffae, 0xffffaf, 0xffffb0, 0xffffb1, 0xffffb2, 0xffffb3, 0xffffb4,
- 0xffffb5, 0xffffb6, 0xffffb7, 0xffffb8, 0xffffb9, 0xffffba, 0xffffbb, 0xffffbc, 0xffffbd,
- 0xffffbe, 0xffffbf, 0xffffc0, 0xffffc1, 0xffffc2, 0xffffc3, 0xffffc4, 0xffffc5, 0xffffc6,
- 0xffffc7, 0xffffc8, 0xffffc9, 0xffffca, 0xffffcb, 0xffffcc, 0xffffcd, 0xffffce, 0xffffcf,
- 0xffffd0, 0xffffd1, 0xffffd2, 0xffffd3, 0xffffd4, 0xffffd5, 0xffffd6, 0xffffd7, 0xffffd8,
- 0xffffd9, 0xffffda, 0xffffdb, 0xffffdc
- };
-
- private static final byte[] RESPONSE_CODE_LENGTHS = {
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 4, 12, 7, 13, 14, 9, 10, 13, 9, 9, 12, 11, 6, 6, 6, 7, 4,
- 4, 4, 5, 5, 5, 6, 6, 5, 5, 5, 9, 16, 7, 13, 12, 16, 7, 8, 8, 8, 8, 8, 6, 9, 8, 8, 10, 9, 6, 8,
- 8, 9, 10, 9, 6, 5, 9, 9, 8, 10, 9, 10, 11, 13, 11, 15, 9, 17, 5, 7, 6, 6, 5, 7, 7, 7, 6, 9, 9,
- 7, 6, 6, 6, 6, 9, 6, 6, 6, 6, 7, 8, 7, 8, 9, 16, 14, 16, 16, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25, 25,
- 25, 25, 25, 25, 25, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
- 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
- 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
- 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
- 24, 24, 24, 24, 24, 24
- };
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
index 293d817..6413f36 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/NameValueBlockReader.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2013 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
package com.squareup.okhttp.internal.spdy;
import java.io.IOException;
@@ -5,11 +20,11 @@
import java.util.List;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;
+import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
-import okio.Deadline;
+import okio.ForwardingSource;
import okio.InflaterSource;
-import okio.OkBuffer;
import okio.Okio;
import okio.Source;
@@ -32,28 +47,18 @@
/** This source holds inflated bytes. */
private final BufferedSource source;
- public NameValueBlockReader(final BufferedSource source) {
+ public NameValueBlockReader(BufferedSource source) {
// Limit the inflater input stream to only those bytes in the Name/Value
// block. We cut the inflater off at its source because we can't predict the
// ratio of compressed bytes to uncompressed bytes.
- Source throttleSource = new Source() {
- @Override public long read(OkBuffer sink, long byteCount)
- throws IOException {
+ Source throttleSource = new ForwardingSource(source) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
if (compressedLimit == 0) return -1; // Out of data for the current block.
- long read = source.read(sink, Math.min(byteCount, compressedLimit));
+ long read = super.read(sink, Math.min(byteCount, compressedLimit));
if (read == -1) return -1;
compressedLimit -= read;
return read;
}
-
- @Override public void close() throws IOException {
- source.close();
- }
-
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
};
// Subclass inflater to install a dictionary when it's needed.
@@ -80,7 +85,7 @@
if (numberOfPairs < 0) throw new IOException("numberOfPairs < 0: " + numberOfPairs);
if (numberOfPairs > 1024) throw new IOException("numberOfPairs > 1024: " + numberOfPairs);
- List<Header> entries = new ArrayList<Header>(numberOfPairs);
+ List<Header> entries = new ArrayList<>(numberOfPairs);
for (int i = 0; i < numberOfPairs; i++) {
ByteString name = readByteString().toAsciiLowercase();
ByteString values = readByteString();
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
index c585255..06b0aef 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Ping.java
@@ -49,7 +49,7 @@
/**
* Returns the round trip time for this ping in nanoseconds, waiting for the
* response to arrive if necessary. Returns -1 if the response was
- * cancelled.
+ * canceled.
*/
public long roundTripTime() throws InterruptedException {
latch.await();
@@ -58,7 +58,7 @@
/**
* Returns the round trip time for this ping in nanoseconds, or -1 if the
- * response was cancelled, or -2 if the timeout elapsed before the round
+ * response was canceled, or -2 if the timeout elapsed before the round
* trip completed.
*/
public long roundTripTime(long timeout, TimeUnit unit) throws InterruptedException {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
index 8eecf6b..cdb51f6 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/PushObserver.java
@@ -21,9 +21,19 @@
/**
* {@link com.squareup.okhttp.Protocol#HTTP_2 HTTP/2} only.
- * Processes server-initiated HTTP requests on the client.
+ * Processes server-initiated HTTP requests on the client. Implementations must
+ * quickly dispatch callbacks to avoid creating a bottleneck.
*
- * <p>Use the stream ID to correlate response headers and data.
+ * <p>While {@link #onReset} may occur at any time, the following callbacks are
+ * expected in order, correlated by stream ID.
+ * <ul>
+ * <li>{@link #onRequest}</li>
+ * <li>{@link #onHeaders} (unless canceled)</li>
+ * <li>{@link #onData} (optional sequence of data frames)</li>
+ * </ul>
+ *
+ * <p>As a stream ID is scoped to a single HTTP/2 connection, implementations
+ * which target multiple connections should expect repetition of stream IDs.
*
* <p>Return true to request cancellation of a pushed stream. Note that this
* does not guarantee future frames won't arrive on the stream ID.
@@ -60,7 +70,7 @@
boolean onData(int streamId, BufferedSource source, int byteCount, boolean last)
throws IOException;
- /** Indicates the reason why this stream was cancelled. */
+ /** Indicates the reason why this stream was canceled. */
void onReset(int streamId, ErrorCode errorCode);
PushObserver CANCEL = new PushObserver() {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
index bf43088..bb67b83 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Settings.java
@@ -21,7 +21,7 @@
* Settings describe characteristics of the sending peer, which are used by the receiving peer.
* Settings are {@link com.squareup.okhttp.internal.spdy.SpdyConnection connection} scoped.
*/
-final class Settings {
+public final class Settings {
/**
* From the SPDY/3 and HTTP/2 specs, the default initial window size for all
* streams is 64 KiB. (Chrome 25 uses 10 MiB).
@@ -38,11 +38,11 @@
/** spdy/3: Sender's estimate of max incoming kbps. */
static final int UPLOAD_BANDWIDTH = 1;
- /** http/2: Size in bytes of the table used to decode the sender's header blocks. */
+ /** HTTP/2: Size in bytes of the table used to decode the sender's header blocks. */
static final int HEADER_TABLE_SIZE = 1;
/** spdy/3: Sender's estimate of max outgoing kbps. */
static final int DOWNLOAD_BANDWIDTH = 2;
- /** http/2: An endpoint must not send a PUSH_PROMISE frame when this is 0. */
+ /** HTTP/2: The peer must not send a PUSH_PROMISE frame when this is 0. */
static final int ENABLE_PUSH = 2;
/** spdy/3: Sender's estimate of millis between sending a request and receiving a response. */
static final int ROUND_TRIP_TIME = 3;
@@ -50,11 +50,15 @@
static final int MAX_CONCURRENT_STREAMS = 4;
/** spdy/3: Current CWND in Packets. */
static final int CURRENT_CWND = 5;
+ /** HTTP/2: Size in bytes of the largest frame payload the sender will accept. */
+ static final int MAX_FRAME_SIZE = 5;
/** spdy/3: Retransmission rate. Percentage */
static final int DOWNLOAD_RETRANS_RATE = 6;
+ /** HTTP/2: Advisory only. Size in bytes of the largest header list the sender will accept. */
+ static final int MAX_HEADER_LIST_SIZE = 6;
/** Window size in bytes. */
static final int INITIAL_WINDOW_SIZE = 7;
- /** spdy/3: Window size in bytes. */
+ /** spdy/3: Size of the client certificate vector. Unsupported. */
static final int CLIENT_CERTIFICATE_VECTOR_SIZE = 8;
/** Flow control options. */
static final int FLOW_CONTROL_OPTIONS = 10;
@@ -134,7 +138,7 @@
return (bit & set) != 0 ? values[UPLOAD_BANDWIDTH] : defaultValue;
}
- /** http/2 only. Returns -1 if unset. */
+ /** HTTP/2 only. Returns -1 if unset. */
int getHeaderTableSize() {
int bit = 1 << HEADER_TABLE_SIZE;
return (bit & set) != 0 ? values[HEADER_TABLE_SIZE] : -1;
@@ -146,8 +150,8 @@
return (bit & set) != 0 ? values[DOWNLOAD_BANDWIDTH] : defaultValue;
}
- /** http/2 only. */
- // TODO: honor this setting in http/2.
+ /** HTTP/2 only. */
+ // TODO: honor this setting in HTTP/2.
boolean getEnablePush(boolean defaultValue) {
int bit = 1 << ENABLE_PUSH;
return ((bit & set) != 0 ? values[ENABLE_PUSH] : defaultValue ? 1 : 0) == 1;
@@ -159,7 +163,7 @@
return (bit & set) != 0 ? values[ROUND_TRIP_TIME] : defaultValue;
}
- // TODO: honor this setting in spdy/3 and http/2.
+ // TODO: honor this setting in spdy/3 and HTTP/2.
int getMaxConcurrentStreams(int defaultValue) {
int bit = 1 << MAX_CONCURRENT_STREAMS;
return (bit & set) != 0 ? values[MAX_CONCURRENT_STREAMS] : defaultValue;
@@ -171,12 +175,24 @@
return (bit & set) != 0 ? values[CURRENT_CWND] : defaultValue;
}
+ /** HTTP/2 only. */
+ int getMaxFrameSize(int defaultValue) {
+ int bit = 1 << MAX_FRAME_SIZE;
+ return (bit & set) != 0 ? values[MAX_FRAME_SIZE] : defaultValue;
+ }
+
/** spdy/3 only. */
int getDownloadRetransRate(int defaultValue) {
int bit = 1 << DOWNLOAD_RETRANS_RATE;
return (bit & set) != 0 ? values[DOWNLOAD_RETRANS_RATE] : defaultValue;
}
+ /** HTTP/2 only. */
+ int getMaxHeaderListSize(int defaultValue) {
+ int bit = 1 << MAX_HEADER_LIST_SIZE;
+ return (bit & set) != 0 ? values[MAX_HEADER_LIST_SIZE] : defaultValue;
+ }
+
int getInitialWindowSize(int defaultValue) {
int bit = 1 << INITIAL_WINDOW_SIZE;
return (bit & set) != 0 ? values[INITIAL_WINDOW_SIZE] : defaultValue;
@@ -188,7 +204,7 @@
return (bit & set) != 0 ? values[CLIENT_CERTIFICATE_VECTOR_SIZE] : defaultValue;
}
- // TODO: honor this setting in spdy/3 and http/2.
+ // TODO: honor this setting in spdy/3 and HTTP/2.
boolean isFlowControlDisabled() {
int bit = 1 << FLOW_CONTROL_OPTIONS;
int value = (bit & set) != 0 ? values[FLOW_CONTROL_OPTIONS] : 0;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
index a71bc6f..c5cebe7 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Spdy3.java
@@ -22,18 +22,18 @@
import java.net.ProtocolException;
import java.util.List;
import java.util.zip.Deflater;
+import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.ByteString;
import okio.DeflaterSink;
-import okio.OkBuffer;
import okio.Okio;
/**
* Read and write spdy/3.1 frames.
* http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1
*/
-final class Spdy3 implements Variant {
+public final class Spdy3 implements Variant {
@Override public Protocol getProtocol() {
return Protocol.SPDY_3;
@@ -103,10 +103,6 @@
return new Writer(sink, client);
}
- @Override public int maxFrameSize() {
- return 16383;
- }
-
/** Read spdy/3 frames. */
static final class Reader implements FrameReader {
private final BufferedSource source;
@@ -119,7 +115,7 @@
this.client = client;
}
- @Override public void readConnectionHeader() {
+ @Override public void readConnectionPreface() {
}
/**
@@ -196,17 +192,15 @@
private void readSynStream(Handler handler, int flags, int length) throws IOException {
int w1 = source.readInt();
int w2 = source.readInt();
- int s3 = source.readShort();
int streamId = w1 & 0x7fffffff;
int associatedStreamId = w2 & 0x7fffffff;
- int priority = (s3 & 0xe000) >>> 13;
- // int slot = s3 & 0xff;
+ source.readShort(); // int priority = (s3 & 0xe000) >>> 13; int slot = s3 & 0xff;
List<Header> headerBlock = headerBlockReader.readNameValueBlock(length - 10);
boolean inFinished = (flags & FLAG_FIN) != 0;
boolean outFinished = (flags & FLAG_UNIDIRECTIONAL) != 0;
- handler.headers(outFinished, inFinished, streamId, associatedStreamId, priority,
- headerBlock, HeadersMode.SPDY_SYN_STREAM);
+ handler.headers(outFinished, inFinished, streamId, associatedStreamId, headerBlock,
+ HeadersMode.SPDY_SYN_STREAM);
}
private void readSynReply(Handler handler, int flags, int length) throws IOException {
@@ -214,7 +208,7 @@
int streamId = w1 & 0x7fffffff;
List<Header> headerBlock = headerBlockReader.readNameValueBlock(length - 4);
boolean inFinished = (flags & FLAG_FIN) != 0;
- handler.headers(false, inFinished, streamId, -1, -1, headerBlock, HeadersMode.SPDY_REPLY);
+ handler.headers(false, inFinished, streamId, -1, headerBlock, HeadersMode.SPDY_REPLY);
}
private void readRstStream(Handler handler, int flags, int length) throws IOException {
@@ -232,7 +226,7 @@
int w1 = source.readInt();
int streamId = w1 & 0x7fffffff;
List<Header> headerBlock = headerBlockReader.readNameValueBlock(length - 4);
- handler.headers(false, false, streamId, -1, -1, headerBlock, HeadersMode.SPDY_HEADERS);
+ handler.headers(false, false, streamId, -1, headerBlock, HeadersMode.SPDY_HEADERS);
}
private void readWindowUpdate(Handler handler, int flags, int length) throws IOException {
@@ -292,7 +286,7 @@
/** Write spdy/3 frames. */
static final class Writer implements FrameWriter {
private final BufferedSink sink;
- private final OkBuffer headerBlockBuffer;
+ private final Buffer headerBlockBuffer;
private final BufferedSink headerBlockOut;
private final boolean client;
private boolean closed;
@@ -303,11 +297,11 @@
Deflater deflater = new Deflater();
deflater.setDictionary(DICTIONARY);
- headerBlockBuffer = new OkBuffer();
+ headerBlockBuffer = new Buffer();
headerBlockOut = Okio.buffer(new DeflaterSink(headerBlockBuffer, deflater));
}
- @Override public void ackSettings() {
+ @Override public void ackSettings(Settings peerSettings) {
// Do nothing: no ACK for SPDY/3 settings.
}
@@ -317,8 +311,8 @@
// Do nothing: no push promise for SPDY/3.
}
- @Override public synchronized void connectionHeader() {
- // Do nothing: no connection header for SPDY/3.
+ @Override public synchronized void connectionPreface() {
+ // Do nothing: no connection preface for SPDY/3.
}
@Override public synchronized void flush() throws IOException {
@@ -327,7 +321,7 @@
}
@Override public synchronized void synStream(boolean outFinished, boolean inFinished,
- int streamId, int associatedStreamId, int priority, int slot, List<Header> headerBlock)
+ int streamId, int associatedStreamId, List<Header> headerBlock)
throws IOException {
if (closed) throw new IOException("closed");
writeNameValueBlockToBuffer(headerBlock);
@@ -340,8 +334,8 @@
sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
sink.writeInt(streamId & 0x7fffffff);
sink.writeInt(associatedStreamId & 0x7fffffff);
- sink.writeShort((priority & 0x7) << 13 | (unused & 0x1f) << 8 | (slot & 0xff));
- sink.write(headerBlockBuffer, headerBlockBuffer.size());
+ sink.writeShort((unused & 0x7) << 13 | (unused & 0x1f) << 8 | (unused & 0xff));
+ sink.writeAll(headerBlockBuffer);
sink.flush();
}
@@ -356,7 +350,7 @@
sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
sink.writeInt(streamId & 0x7fffffff);
- sink.write(headerBlockBuffer, headerBlockBuffer.size());
+ sink.writeAll(headerBlockBuffer);
sink.flush();
}
@@ -371,7 +365,7 @@
sink.writeInt(0x80000000 | (VERSION & 0x7fff) << 16 | type & 0xffff);
sink.writeInt((flags & 0xff) << 24 | length & 0xffffff);
sink.writeInt(streamId & 0x7fffffff);
- sink.write(headerBlockBuffer, headerBlockBuffer.size());
+ sink.writeAll(headerBlockBuffer);
}
@Override public synchronized void rstStream(int streamId, ErrorCode errorCode)
@@ -388,18 +382,17 @@
sink.flush();
}
- @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source)
- throws IOException {
- data(outFinished, streamId, source, (int) source.size());
+ @Override public int maxDataLength() {
+ return 16383;
}
- @Override public synchronized void data(boolean outFinished, int streamId, OkBuffer source,
+ @Override public synchronized void data(boolean outFinished, int streamId, Buffer source,
int byteCount) throws IOException {
int flags = (outFinished ? FLAG_FIN : 0);
sendDataFrame(streamId, flags, source, byteCount);
}
- void sendDataFrame(int streamId, int flags, OkBuffer buffer, int byteCount)
+ void sendDataFrame(int streamId, int flags, Buffer buffer, int byteCount)
throws IOException {
if (closed) throw new IOException("closed");
if (byteCount > 0xffffffL) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
index da7c4e1..e7ab873 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyConnection.java
@@ -24,19 +24,18 @@
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.HashMap;
-import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
-import okio.BufferedSink;
+import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
-import okio.OkBuffer;
import okio.Okio;
import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
@@ -79,13 +78,16 @@
* run on the callback executor.
*/
private final IncomingStreamHandler handler;
- private final Map<Integer, SpdyStream> streams = new HashMap<Integer, SpdyStream>();
+ private final Map<Integer, SpdyStream> streams = new HashMap<>();
private final String hostName;
private int lastGoodStreamId;
private int nextStreamId;
private boolean shutdown;
private long idleStartTimeNs = System.nanoTime();
+ /** Ensures push promise callbacks events are sent in order per stream. */
+ private final ExecutorService pushExecutor;
+
/** Lazily-created map of in-flight pings awaiting a response. Guarded by this. */
private Map<Integer, Ping> pings;
/** User code to run in response to push promise events. */
@@ -110,25 +112,31 @@
// 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);
+ private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024;
/** Settings we receive from the peer. */
// TODO: MWS will need to guard on this setting before attempting to push.
final Settings peerSettings = new Settings();
private boolean receivedInitialPeerSettings = false;
- final FrameReader frameReader;
+ final Variant variant;
+ final Socket socket;
final FrameWriter frameWriter;
- final long maxFrameSize;
// Visible for testing
final Reader readerRunnable;
- private SpdyConnection(Builder builder) {
+ private SpdyConnection(Builder builder) throws IOException {
protocol = builder.protocol;
pushObserver = builder.pushObserver;
client = builder.client;
handler = builder.handler;
+ // http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.1.1
nextStreamId = builder.client ? 1 : 2;
+ if (builder.client && protocol == Protocol.HTTP_2) {
+ nextStreamId += 2; // In HTTP/2, 1 on client is reserved for Upgrade.
+ }
+
nextPingId = builder.client ? 1 : 2;
// Flow control was designed more for servers, or proxies than edge clients.
@@ -136,29 +144,35 @@
// thrashing window updates every 64KiB, yet small enough to avoid blowing
// up the heap.
if (builder.client) {
- okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 16 * 1024 * 1024);
+ okHttpSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, OKHTTP_CLIENT_WINDOW_SIZE);
}
hostName = builder.hostName;
- Variant variant;
if (protocol == Protocol.HTTP_2) {
- variant = new Http20Draft09();
+ variant = new Http20Draft16();
+ // Like newSingleThreadExecutor, except lazy creates the thread.
+ pushExecutor = new ThreadPoolExecutor(0, 1, 60, TimeUnit.SECONDS,
+ new LinkedBlockingQueue<Runnable>(),
+ Util.threadFactory(String.format("OkHttp %s Push Observer", hostName), true));
+ // 1 less than SPDY http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-6.9.2
+ peerSettings.set(Settings.INITIAL_WINDOW_SIZE, 0, 65535);
+ peerSettings.set(Settings.MAX_FRAME_SIZE, 0, Http20Draft16.INITIAL_MAX_FRAME_SIZE);
} else if (protocol == Protocol.SPDY_3) {
variant = new Spdy3();
+ pushExecutor = null;
} else {
throw new AssertionError(protocol);
}
bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
- frameReader = variant.newReader(builder.source, client);
- frameWriter = variant.newWriter(builder.sink, client);
- maxFrameSize = variant.maxFrameSize();
+ socket = builder.socket;
+ frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client);
readerRunnable = new Reader();
new Thread(readerRunnable).start(); // Not a daemon thread.
}
- /** The protocol as selected using NPN or ALPN. */
+ /** The protocol as selected using ALPN. */
public Protocol getProtocol() {
return protocol;
}
@@ -232,8 +246,6 @@
boolean in) throws IOException {
boolean outFinished = !out;
boolean inFinished = !in;
- int priority = -1; // TODO: permit the caller to specify a priority?
- int slot = 0; // TODO: permit the caller to specify a slot?
SpdyStream stream;
int streamId;
@@ -244,14 +256,14 @@
}
streamId = nextStreamId;
nextStreamId += 2;
- stream = new SpdyStream(streamId, this, outFinished, inFinished, priority, requestHeaders);
+ stream = new SpdyStream(streamId, this, outFinished, inFinished, requestHeaders);
if (stream.isOpen()) {
streams.put(streamId, stream);
setIdle(false);
}
}
if (associatedStreamId == 0) {
- frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId, priority, slot,
+ frameWriter.synStream(outFinished, inFinished, streamId, associatedStreamId,
requestHeaders);
} else if (client) {
throw new IllegalArgumentException("client streams shouldn't have associated stream IDs");
@@ -287,7 +299,7 @@
* will not block. The only use case for zero {@code byteCount} is closing
* a flushed output stream.
*/
- public void writeData(int streamId, boolean outFinished, OkBuffer buffer, long byteCount)
+ public void writeData(int streamId, boolean outFinished, Buffer buffer, long byteCount)
throws IOException {
if (byteCount == 0) { // Empty data frames are not flow-controlled.
frameWriter.data(outFinished, streamId, buffer, 0);
@@ -305,7 +317,8 @@
throw new InterruptedIOException();
}
- toWrite = (int) Math.min(Math.min(byteCount, bytesLeftInWriteWindow), maxFrameSize);
+ toWrite = (int) Math.min(byteCount, bytesLeftInWriteWindow);
+ toWrite = Math.min(toWrite, frameWriter.maxDataLength());
bytesLeftInWriteWindow -= toWrite;
}
@@ -362,7 +375,7 @@
}
pingId = nextPingId;
nextPingId += 2;
- if (pings == null) pings = new HashMap<Integer, Ping>();
+ if (pings == null) pings = new HashMap<>();
pings.put(pingId, ping);
}
writePing(false, pingId, 0x4f4b6f6b /* ASCII "OKok" */, ping);
@@ -467,17 +480,20 @@
}
}
- try {
- frameReader.close();
- } catch (IOException e) {
- thrown = e;
- }
+ // Close the writer to release its resources (such as deflaters).
try {
frameWriter.close();
} catch (IOException e) {
if (thrown == null) thrown = e;
}
+ // Close the socket to break out the reader thread, which will clean up after itself.
+ try {
+ socket.close();
+ } catch (IOException e) {
+ thrown = e;
+ }
+
if (thrown != null) throw thrown;
}
@@ -485,15 +501,18 @@
* Sends a connection header if the current variant requires it. This should
* be called after {@link Builder#build} for all new connections.
*/
- public void sendConnectionHeader() throws IOException {
- frameWriter.connectionHeader();
+ public void sendConnectionPreface() throws IOException {
+ frameWriter.connectionPreface();
frameWriter.settings(okHttpSettings);
+ int windowSize = okHttpSettings.getInitialWindowSize(Settings.DEFAULT_INITIAL_WINDOW_SIZE);
+ if (windowSize != Settings.DEFAULT_INITIAL_WINDOW_SIZE) {
+ frameWriter.windowUpdate(0, windowSize - Settings.DEFAULT_INITIAL_WINDOW_SIZE);
+ }
}
public static class Builder {
private String hostName;
- private BufferedSource source;
- private BufferedSink sink;
+ private Socket socket;
private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
private Protocol protocol = Protocol.SPDY_3;
private PushObserver pushObserver = PushObserver.CANCEL;
@@ -510,8 +529,7 @@
public Builder(String hostName, boolean client, Socket socket) throws IOException {
this.hostName = hostName;
this.client = client;
- this.source = Okio.buffer(Okio.source(socket.getInputStream()));
- this.sink = Okio.buffer(Okio.sink(socket.getOutputStream()));
+ this.socket = socket;
}
public Builder handler(IncomingStreamHandler handler) {
@@ -529,7 +547,7 @@
return this;
}
- public SpdyConnection build() {
+ public SpdyConnection build() throws IOException {
return new SpdyConnection(this);
}
}
@@ -539,6 +557,8 @@
* write a frame, create an async task to do so.
*/
class Reader extends NamedRunnable implements FrameReader.Handler {
+ FrameReader frameReader;
+
private Reader() {
super("OkHttp %s", hostName);
}
@@ -547,8 +567,9 @@
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
+ frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client);
if (!client) {
- frameReader.readConnectionHeader();
+ frameReader.readConnectionPreface();
}
while (frameReader.nextFrame(this)) {
}
@@ -562,6 +583,7 @@
close(connectionErrorCode, streamErrorCode);
} catch (IOException ignored) {
}
+ Util.closeQuietly(frameReader);
}
}
@@ -584,7 +606,7 @@
}
@Override public void headers(boolean outFinished, boolean inFinished, int streamId,
- int associatedStreamId, int priority, List<Header> headerBlock, HeadersMode headersMode) {
+ int associatedStreamId, List<Header> headerBlock, HeadersMode headersMode) {
if (pushedStream(streamId)) {
pushHeadersLater(streamId, headerBlock, inFinished);
return;
@@ -611,7 +633,7 @@
// Create a stream.
final SpdyStream newStream = new SpdyStream(streamId, SpdyConnection.this, outFinished,
- inFinished, priority, headerBlock);
+ inFinished, headerBlock);
lastGoodStreamId = streamId;
streams.put(streamId, newStream);
executor.submit(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) {
@@ -658,7 +680,7 @@
if (clearPrevious) peerSettings.clear();
peerSettings.merge(newSettings);
if (getProtocol() == Protocol.HTTP_2) {
- ackSettingsLater();
+ ackSettingsLater(newSettings);
}
int peerInitialWindowSize = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
if (peerInitialWindowSize != -1 && peerInitialWindowSize != priorWriteWindowSize) {
@@ -673,7 +695,7 @@
}
}
if (streamsToNotify != null && delta != 0) {
- for (SpdyStream stream : streams.values()) {
+ for (SpdyStream stream : streamsToNotify) {
synchronized (stream) {
stream.addBytesToWriteWindow(delta);
}
@@ -681,11 +703,11 @@
}
}
- private void ackSettingsLater() {
+ private void ackSettingsLater(final Settings peerSettings) {
executor.submit(new NamedRunnable("OkHttp %s ACK Settings", hostName) {
@Override public void execute() {
try {
- frameWriter.ackSettings();
+ frameWriter.ackSettings(peerSettings);
} catch (IOException ignored) {
}
}
@@ -711,18 +733,19 @@
@Override public void goAway(int lastGoodStreamId, ErrorCode errorCode, ByteString debugData) {
if (debugData.size() > 0) { // TODO: log the debugData
}
- synchronized (SpdyConnection.this) {
- shutdown = true;
- // Fail all streams created after the last good stream ID.
- for (Iterator<Map.Entry<Integer, SpdyStream>> i = streams.entrySet().iterator();
- i.hasNext(); ) {
- Map.Entry<Integer, SpdyStream> entry = i.next();
- int streamId = entry.getKey();
- if (streamId > lastGoodStreamId && entry.getValue().isLocallyInitiated()) {
- entry.getValue().receiveRstStream(ErrorCode.REFUSED_STREAM);
- i.remove();
- }
+ // Copy the streams first. We don't want to hold a lock when we call receiveRstStream().
+ SpdyStream[] streamsCopy;
+ synchronized (SpdyConnection.this) {
+ streamsCopy = streams.values().toArray(new SpdyStream[streams.size()]);
+ shutdown = true;
+ }
+
+ // Fail all streams created after the last good stream ID.
+ for (SpdyStream spdyStream : streamsCopy) {
+ if (spdyStream.getId() > lastGoodStreamId && spdyStream.isLocallyInitiated()) {
+ spdyStream.receiveRstStream(ErrorCode.REFUSED_STREAM);
+ removeStream(spdyStream.getId());
}
}
}
@@ -743,7 +766,8 @@
}
}
- @Override public void priority(int streamId, int priority) {
+ @Override public void priority(int streamId, int streamDependency, int weight,
+ boolean exclusive) {
// TODO: honor priority.
}
@@ -751,6 +775,11 @@
public void pushPromise(int streamId, int promisedStreamId, List<Header> requestHeaders) {
pushRequestLater(promisedStreamId, requestHeaders);
}
+
+ @Override public void alternateService(int streamId, String origin, ByteString protocol,
+ String host, int port, long maxAge) {
+ // TODO: register alternate service.
+ }
}
/** Even, positive numbered streams are pushed streams in HTTP/2. */
@@ -759,7 +788,7 @@
}
// Guarded by this.
- private final Set<Integer> currentPushRequests = new LinkedHashSet<Integer>();
+ private final Set<Integer> currentPushRequests = new LinkedHashSet<>();
private void pushRequestLater(final int streamId, final List<Header> requestHeaders) {
synchronized (this) {
@@ -769,7 +798,7 @@
}
currentPushRequests.add(streamId);
}
- executor.submit(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) {
+ pushExecutor.submit(new NamedRunnable("OkHttp %s Push Request[%s]", hostName, streamId) {
@Override public void execute() {
boolean cancel = pushObserver.onRequest(streamId, requestHeaders);
try {
@@ -787,7 +816,7 @@
private void pushHeadersLater(final int streamId, final List<Header> requestHeaders,
final boolean inFinished) {
- executor.submit(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) {
+ pushExecutor.submit(new NamedRunnable("OkHttp %s Push Headers[%s]", hostName, streamId) {
@Override public void execute() {
boolean cancel = pushObserver.onHeaders(streamId, requestHeaders, inFinished);
try {
@@ -809,11 +838,11 @@
*/
private void pushDataLater(final int streamId, final BufferedSource source, final int byteCount,
final boolean inFinished) throws IOException {
- final OkBuffer buffer = new OkBuffer();
+ final Buffer buffer = new Buffer();
source.require(byteCount); // Eagerly read the frame before firing client thread.
source.read(buffer, byteCount);
if (buffer.size() != byteCount) throw new IOException(buffer.size() + " != " + byteCount);
- executor.submit(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) {
+ pushExecutor.submit(new NamedRunnable("OkHttp %s Push Data[%s]", hostName, streamId) {
@Override public void execute() {
try {
boolean cancel = pushObserver.onData(streamId, buffer, byteCount, inFinished);
@@ -830,7 +859,7 @@
}
private void pushResetLater(final int streamId, final ErrorCode errorCode) {
- executor.submit(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) {
+ pushExecutor.submit(new NamedRunnable("OkHttp %s Push Reset[%s]", hostName, streamId) {
@Override public void execute() {
pushObserver.onReset(streamId, errorCode);
synchronized (SpdyConnection.this) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
index 0fcde2d..331536d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/SpdyStream.java
@@ -19,14 +19,14 @@
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
-import java.net.SocketTimeoutException;
import java.util.ArrayList;
import java.util.List;
+import okio.AsyncTimeout;
+import okio.Buffer;
import okio.BufferedSource;
-import okio.Deadline;
-import okio.OkBuffer;
import okio.Sink;
import okio.Source;
+import okio.Timeout;
import static com.squareup.okhttp.internal.spdy.Settings.DEFAULT_INITIAL_WINDOW_SIZE;
@@ -53,7 +53,6 @@
private final int id;
private final SpdyConnection connection;
- private final int priority;
private long readTimeoutMillis = 0;
/** Headers sent by the stream initiator. Immutable and non null. */
@@ -64,6 +63,8 @@
private final SpdyDataSource source;
final SpdyDataSink sink;
+ private final SpdyTimeout readTimeout = new SpdyTimeout();
+ private final SpdyTimeout writeTimeout = new SpdyTimeout();
/**
* The reason why this stream was abnormally closed. If there are multiple
@@ -73,7 +74,7 @@
private ErrorCode errorCode = null;
SpdyStream(int id, SpdyConnection connection, boolean outFinished, boolean inFinished,
- int priority, List<Header> requestHeaders) {
+ List<Header> requestHeaders) {
if (connection == null) throw new NullPointerException("connection == null");
if (requestHeaders == null) throw new NullPointerException("requestHeaders == null");
this.id = id;
@@ -85,7 +86,6 @@
this.sink = new SpdyDataSink();
this.source.finished = inFinished;
this.sink.finished = outFinished;
- this.priority = priority;
this.requestHeaders = requestHeaders;
}
@@ -134,33 +134,16 @@
* have not been received yet.
*/
public synchronized List<Header> getResponseHeaders() throws IOException {
- long remaining = 0;
- long start = 0;
- if (readTimeoutMillis != 0) {
- start = (System.nanoTime() / 1000000);
- remaining = readTimeoutMillis;
- }
+ readTimeout.enter();
try {
while (responseHeaders == null && errorCode == null) {
- if (readTimeoutMillis == 0) { // No timeout configured.
- wait();
- } else if (remaining > 0) {
- wait(remaining);
- remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
- } else {
- throw new SocketTimeoutException("Read response header timeout. readTimeoutMillis: "
- + readTimeoutMillis);
- }
+ waitForIo();
}
- if (responseHeaders != null) {
- return responseHeaders;
- }
- throw new IOException("stream was reset: " + errorCode);
- } catch (InterruptedException e) {
- InterruptedIOException rethrow = new InterruptedIOException();
- rethrow.initCause(e);
- throw rethrow;
+ } finally {
+ readTimeout.exitAndThrowIfTimedOut();
}
+ if (responseHeaders != null) return responseHeaders;
+ throw new IOException("stream was reset: " + errorCode);
}
/**
@@ -200,16 +183,12 @@
}
}
- /**
- * Sets the maximum time to wait on input stream reads before failing with a
- * {@code SocketTimeoutException}, or {@code 0} to wait indefinitely.
- */
- public void setReadTimeout(long readTimeoutMillis) {
- this.readTimeoutMillis = readTimeoutMillis;
+ public Timeout readTimeout() {
+ return readTimeout;
}
- public long getReadTimeoutMillis() {
- return readTimeoutMillis;
+ public Timeout writeTimeout() {
+ return writeTimeout;
}
/** Returns a source that reads data from the peer. */
@@ -288,7 +267,7 @@
if (headersMode.failIfHeadersPresent()) {
errorCode = ErrorCode.STREAM_IN_USE;
} else {
- List<Header> newHeaders = new ArrayList<Header>();
+ List<Header> newHeaders = new ArrayList<>();
newHeaders.addAll(responseHeaders);
newHeaders.addAll(headers);
this.responseHeaders = newHeaders;
@@ -327,10 +306,6 @@
}
}
- int getPriority() {
- return priority;
- }
-
/**
* A source that reads the incoming data frames of a stream. Although this
* class uses synchronization to safely receive incoming data frames, it is
@@ -338,10 +313,10 @@
*/
private final class SpdyDataSource implements Source {
/** Buffer to receive data from the network into. Only accessed by the reader thread. */
- private final OkBuffer receiveBuffer = new OkBuffer();
+ private final Buffer receiveBuffer = new Buffer();
/** Buffer with readable data. Guarded by SpdyStream.this. */
- private final OkBuffer readBuffer = new OkBuffer();
+ private final Buffer readBuffer = new Buffer();
/** Maximum number of bytes to buffer before reporting a flow control error. */
private final long maxByteCount;
@@ -359,7 +334,7 @@
this.maxByteCount = maxByteCount;
}
- @Override public long read(OkBuffer sink, long byteCount)
+ @Override public long read(Buffer sink, long byteCount)
throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
@@ -375,7 +350,7 @@
// Flow control: notify the peer that we're ready for more data!
unacknowledgedBytesRead += read;
if (unacknowledgedBytesRead
- >= connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
+ >= connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
connection.writeWindowUpdateLater(id, unacknowledgedBytesRead);
unacknowledgedBytesRead = 0;
}
@@ -385,7 +360,7 @@
synchronized (connection) { // Multiple application threads may hit this section.
connection.unacknowledgedBytesRead += read;
if (connection.unacknowledgedBytesRead
- >= connection.peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
+ >= connection.okHttpSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE) / 2) {
connection.writeWindowUpdateLater(0, connection.unacknowledgedBytesRead);
connection.unacknowledgedBytesRead = 0;
}
@@ -394,31 +369,15 @@
return read;
}
- /**
- * Returns once the input stream is either readable or finished. Throws
- * a {@link SocketTimeoutException} if the read timeout elapses before
- * that happens.
- */
+ /** Returns once the source is either readable or finished. */
private void waitUntilReadable() throws IOException {
- long start = 0;
- long remaining = 0;
- if (readTimeoutMillis != 0) {
- start = (System.nanoTime() / 1000000);
- remaining = readTimeoutMillis;
- }
+ readTimeout.enter();
try {
while (readBuffer.size() == 0 && !finished && !closed && errorCode == null) {
- if (readTimeoutMillis == 0) {
- SpdyStream.this.wait();
- } else if (remaining > 0) {
- SpdyStream.this.wait(remaining);
- remaining = start + readTimeoutMillis - (System.nanoTime() / 1000000);
- } else {
- throw new SocketTimeoutException("Read timed out");
- }
+ waitForIo();
}
- } catch (InterruptedException e) {
- throw new InterruptedIOException();
+ } finally {
+ readTimeout.exitAndThrowIfTimedOut();
}
}
@@ -454,7 +413,7 @@
// Move the received data to the read buffer to the reader can read it.
synchronized (SpdyStream.this) {
boolean wasEmpty = readBuffer.size() == 0;
- readBuffer.write(receiveBuffer, receiveBuffer.size());
+ readBuffer.writeAll(receiveBuffer);
if (wasEmpty) {
SpdyStream.this.notifyAll();
}
@@ -462,9 +421,8 @@
}
}
- @Override public Source deadline(Deadline deadline) {
- // TODO: honor deadlines.
- return this;
+ @Override public Timeout timeout() {
+ return readTimeout;
}
@Override public void close() throws IOException {
@@ -518,17 +476,18 @@
*/
private boolean finished;
- @Override public void write(OkBuffer source, long byteCount) throws IOException {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
assert (!Thread.holdsLock(SpdyStream.this));
while (byteCount > 0) {
long toWrite;
synchronized (SpdyStream.this) {
+ writeTimeout.enter();
try {
- while (bytesLeftInWriteWindow <= 0) {
- SpdyStream.this.wait(); // Wait until we receive a WINDOW_UPDATE.
+ while (bytesLeftInWriteWindow <= 0 && !finished && !closed && errorCode == null) {
+ waitForIo(); // Wait until we receive a WINDOW_UPDATE.
}
- } catch (InterruptedException e) {
- throw new InterruptedIOException();
+ } finally {
+ writeTimeout.exitAndThrowIfTimedOut();
}
checkOutNotClosed(); // Kick out if the stream was reset or closed while waiting.
@@ -549,9 +508,8 @@
connection.flush();
}
- @Override public Sink deadline(Deadline deadline) {
- // TODO: honor deadlines.
- return this;
+ @Override public Timeout timeout() {
+ return writeTimeout;
}
@Override public void close() throws IOException {
@@ -588,4 +546,31 @@
throw new IOException("stream was reset: " + errorCode);
}
}
+
+ /**
+ * Like {@link #wait}, but throws an {@code InterruptedIOException} when
+ * interrupted instead of the more awkward {@link InterruptedException}.
+ */
+ private void waitForIo() throws InterruptedIOException {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ throw new InterruptedIOException();
+ }
+ }
+
+ /**
+ * The Okio timeout watchdog will call {@link #timedOut} if the timeout is
+ * reached. In that case we close the stream (asynchronously) which will
+ * notify the waiting thread.
+ */
+ class SpdyTimeout extends AsyncTimeout {
+ @Override protected void timedOut() {
+ closeLater(ErrorCode.CANCEL);
+ }
+
+ public void exitAndThrowIfTimedOut() throws InterruptedIOException {
+ if (exit()) throw new InterruptedIOException("timeout");
+ }
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
index f8b14ac..c4b082d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/spdy/Variant.java
@@ -20,9 +20,9 @@
import okio.BufferedSource;
/** A version and dialect of the framed socket protocol. */
-interface Variant {
+public interface Variant {
- /** The protocol as selected using NPN or ALPN. */
+ /** The protocol as selected using ALPN. */
Protocol getProtocol();
/**
@@ -34,6 +34,4 @@
* @param client true if this is the HTTP client's writer, writing frames to a server.
*/
FrameWriter newWriter(BufferedSink sink, boolean client);
-
- int maxFrameSize();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
index 10dbd93..740de1b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/OkHostnameVerifier.java
@@ -82,8 +82,9 @@
* Returns true if {@code certificate} matches {@code ipAddress}.
*/
private boolean verifyIpAddress(String ipAddress, X509Certificate certificate) {
- for (String altName : getSubjectAltNames(certificate, ALT_IPA_NAME)) {
- if (ipAddress.equalsIgnoreCase(altName)) {
+ List<String> altNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
+ for (int i = 0, size = altNames.size(); i < size; i++) {
+ if (ipAddress.equalsIgnoreCase(altNames.get(i))) {
return true;
}
}
@@ -96,9 +97,10 @@
private boolean verifyHostName(String hostName, X509Certificate certificate) {
hostName = hostName.toLowerCase(Locale.US);
boolean hasDns = false;
- for (String altName : getSubjectAltNames(certificate, ALT_DNS_NAME)) {
+ List<String> altNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
+ for (int i = 0, size = altNames.size(); i < size; i++) {
hasDns = true;
- if (verifyHostName(hostName, altName)) {
+ if (verifyHostName(hostName, altNames.get(i))) {
return true;
}
}
@@ -115,8 +117,17 @@
return false;
}
- private List<String> getSubjectAltNames(X509Certificate certificate, int type) {
- List<String> result = new ArrayList<String>();
+ public static List<String> allSubjectAltNames(X509Certificate certificate) {
+ List<String> altIpaNames = getSubjectAltNames(certificate, ALT_IPA_NAME);
+ List<String> altDnsNames = getSubjectAltNames(certificate, ALT_DNS_NAME);
+ List<String> result = new ArrayList<>(altIpaNames.size() + altDnsNames.size());
+ result.addAll(altIpaNames);
+ result.addAll(altDnsNames);
+ return result;
+ }
+
+ private static List<String> getSubjectAltNames(X509Certificate certificate, int type) {
+ List<String> result = new ArrayList<>();
try {
Collection<?> subjectAltNames = certificate.getSubjectAlternativeNames();
if (subjectAltNames == null) {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
new file mode 100644
index 0000000..a926ebc
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import com.squareup.okhttp.internal.NamedRunnable;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.util.Random;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadPoolExecutor;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+
+import static com.squareup.okhttp.internal.ws.WebSocketReader.FrameCallback;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+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;
+ private final WebSocketReader reader;
+ private final WebSocketListener listener;
+
+ /** True after calling {@link #close(int, String)}. No writes are allowed afterward. */
+ private volatile boolean writerSentClose;
+ /** 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();
+
+ public RealWebSocket(boolean isClient, BufferedSource source, BufferedSink sink, Random random,
+ final WebSocketListener listener, final String url) {
+ this.listener = listener;
+
+ // Pings come in on the reader thread. This executor contends with callers for writing pongs.
+ final ThreadPoolExecutor pongExecutor = new ThreadPoolExecutor(1, 1, 1, SECONDS,
+ new LinkedBlockingDeque<Runnable>(),
+ Util.threadFactory(String.format("OkHttp %s WebSocket", url), true));
+ pongExecutor.allowCoreThreadTimeOut(true);
+
+ 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 onPing(final Buffer buffer) {
+ pongExecutor.execute(new NamedRunnable("OkHttp %s WebSocket Pong", url) {
+ @Override protected void execute() {
+ try {
+ writer.writePong(buffer);
+ } catch (IOException ignored) {
+ }
+ }
+ });
+ }
+
+ @Override public void onPong(Buffer buffer) {
+ listener.onPong(buffer);
+ }
+
+ @Override public void onClose(int code, String reason) throws IOException {
+ peerClose(code, reason);
+ }
+ });
+ }
+
+ /**
+ * Read a single message from the web socket and deliver it to the listener. This method should
+ * be called in a loop with the return value indicating whether looping should continue.
+ */
+ public boolean readMessage() {
+ try {
+ reader.processNextFrame();
+ return !readerSentClose;
+ } catch (IOException e) {
+ readerErrorClose(e);
+ return false;
+ }
+ }
+
+ @Override public BufferedSink newMessageSink(PayloadType type) {
+ if (writerSentClose) throw new IllegalStateException("Closed");
+ return writer.newMessageSink(type);
+ }
+
+ @Override public void sendMessage(PayloadType type, Buffer payload) throws IOException {
+ if (writerSentClose) throw new IllegalStateException("Closed");
+ writer.sendMessage(type, payload);
+ }
+
+ @Override public void sendPing(Buffer payload) throws IOException {
+ if (writerSentClose) throw new IllegalStateException("Closed");
+ writer.writePing(payload);
+ }
+
+ /** Send an unsolicited pong with the specified payload. */
+ public void sendPong(Buffer payload) throws IOException {
+ if (writerSentClose) throw new IllegalStateException("Closed");
+ writer.writePong(payload);
+ }
+
+ @Override public void close(int code, String reason) throws IOException {
+ if (writerSentClose) throw new IllegalStateException("Closed");
+
+ 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();
+ }
+ }
+
+ /** Called on the reader thread when a close frame is encountered. */
+ private void peerClose(int code, String reason) throws IOException {
+ boolean writeCloseResponse;
+ synchronized (closeLock) {
+ readerSentClose = true;
+
+ // If the writer has not indicated a desire to close we will write a close response.
+ writeCloseResponse = !writerSentClose;
+ }
+
+ if (writeCloseResponse) {
+ // The reader thread will read no more frames so use it to send the response.
+ writer.writeClose(code, reason);
+ }
+
+ closeConnection();
+
+ listener.onClose(code, reason);
+ }
+
+ /** 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) {
+ }
+ }
+ }
+
+ try {
+ closeConnection();
+ } catch (IOException ignored) {
+ }
+
+ listener.onFailure(e);
+ }
+
+ /** Perform any tear-down work on the connection (close the socket, recycle, etc.). */
+ protected abstract void closeConnection() throws IOException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocket.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocket.java
new file mode 100644
index 0000000..3be790d
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocket.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import java.io.IOException;
+import okio.Buffer;
+import okio.BufferedSink;
+
+// TODO move to public API!
+/** 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
+ }
+
+ /**
+ * 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.
+ *
+ * @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;
+
+ /**
+ * Send a ping to the server with optional payload.
+ *
+ * @throws IllegalStateException if already closed.
+ */
+ void sendPing(Buffer payload) throws IOException;
+
+ /**
+ * Send a close frame to the server.
+ * <p>
+ * The corresponding {@link WebSocketListener} will continue to get messages until its
+ * {@link WebSocketListener#onClose onClose()} method is called.
+ * <p>
+ * 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 IllegalStateException if already closed.
+ */
+ void close(int code, String reason) throws IOException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketCall.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketCall.java
new file mode 100644
index 0000000..9147e5b
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketCall.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+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.NamedRunnable;
+import com.squareup.okhttp.internal.Util;
+import java.io.IOException;
+import java.net.ProtocolException;
+import java.net.Socket;
+import java.security.SecureRandom;
+import java.util.Collections;
+import java.util.Random;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.ByteString;
+import okio.Okio;
+
+// TODO move to public API!
+public class WebSocketCall {
+ /**
+ * Prepares the {@code request} to create a web socket at some point in the future.
+ * <p>
+ * TODO Move to OkHttpClient as non-static once web sockets are finalized!
+ */
+ public static WebSocketCall newWebSocketCall(OkHttpClient client, Request request) {
+ return new WebSocketCall(client, request);
+ }
+
+ private final Request request;
+ private final Call call;
+ private final Random random;
+ private final String key;
+
+ protected WebSocketCall(OkHttpClient client, Request request) {
+ this(client, request, new SecureRandom());
+ }
+
+ WebSocketCall(OkHttpClient client, Request request, Random random) {
+ if (!"GET".equals(request.method())) {
+ throw new IllegalArgumentException("Request must be GET: " + request.method());
+ }
+ String url = request.urlString();
+ String httpUrl;
+ if (url.startsWith("ws://")) {
+ httpUrl = "http://" + url.substring(5);
+ } else if (url.startsWith("wss://")) {
+ httpUrl = "https://" + url.substring(6);
+ } else if (url.startsWith("http://") || url.startsWith("https://")) {
+ httpUrl = url;
+ } else {
+ throw new IllegalArgumentException(
+ "Request url must use 'ws', 'wss', 'http', or 'https' scheme: " + url);
+ }
+
+ this.random = random;
+
+ byte[] nonce = new byte[16];
+ random.nextBytes(nonce);
+ key = ByteString.of(nonce).base64();
+
+ // Copy the client. Otherwise changes (socket factory, redirect policy,
+ // etc.) may incorrectly be reflected in the request when it is executed.
+ client = client.clone();
+ // Force HTTP/1.1 until the WebSocket over HTTP/2 version is finalized.
+ client.setProtocols(Collections.singletonList(com.squareup.okhttp.Protocol.HTTP_1_1));
+
+ request = request.newBuilder()
+ .url(httpUrl)
+ .header("Upgrade", "websocket")
+ .header("Connection", "Upgrade")
+ .header("Sec-WebSocket-Key", key)
+ .header("Sec-WebSocket-Version", "13")
+ .build();
+ this.request = request;
+
+ call = client.newCall(request);
+ }
+
+ /**
+ * Schedules the request to be executed at some point in the future.
+ *
+ * <p>The {@link OkHttpClient#getDispatcher dispatcher} defines when the request will run:
+ * usually immediately unless there are several other requests currently being executed.
+ *
+ * <p>This client will later call back {@code responseCallback} with either an HTTP response or a
+ * failure exception. If you {@link #cancel} a request before it completes the callback will not
+ * be invoked.
+ *
+ * @throws IllegalStateException when the call has already been executed.
+ */
+ public void enqueue(final WebSocketListener listener) {
+ Callback responseCallback = new Callback() {
+ @Override public void onResponse(Response response) throws IOException {
+ try {
+ createWebSocket(response, listener);
+ } catch (IOException e) {
+ listener.onFailure(e);
+ }
+ }
+
+ @Override public void onFailure(Request request, IOException e) {
+ listener.onFailure(e);
+ }
+ };
+ // TODO call.enqueue(responseCallback, true);
+ Internal.instance.callEnqueue(call, responseCallback, true);
+ }
+
+ /** Cancels the request, if possible. Requests that are already complete cannot be canceled. */
+ public void cancel() {
+ call.cancel();
+ }
+
+ private void createWebSocket(Response response, WebSocketListener listener)
+ throws IOException {
+ if (response.code() != 101) {
+ // TODO call.engine.releaseConnection();
+ Internal.instance.callEngineReleaseConnection(call);
+ throw new ProtocolException("Expected HTTP 101 response but was '"
+ + response.code()
+ + " "
+ + response.message()
+ + "'");
+ }
+
+ String headerConnection = response.header("Connection");
+ if (!"Upgrade".equalsIgnoreCase(headerConnection)) {
+ throw new ProtocolException(
+ "Expected 'Connection' header value 'Upgrade' but was '" + headerConnection + "'");
+ }
+ String headerUpgrade = response.header("Upgrade");
+ if (!"websocket".equalsIgnoreCase(headerUpgrade)) {
+ throw new ProtocolException(
+ "Expected 'Upgrade' header value 'websocket' but was '" + headerUpgrade + "'");
+ }
+ String headerAccept = response.header("Sec-WebSocket-Accept");
+ String acceptExpected = Util.shaBase64(key + WebSocketProtocol.ACCEPT_MAGIC);
+ if (!acceptExpected.equals(headerAccept)) {
+ throw new ProtocolException("Expected 'Sec-WebSocket-Accept' header value '"
+ + acceptExpected
+ + "' but was '"
+ + headerAccept
+ + "'");
+ }
+
+ // 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.");
+ }
+
+ Socket socket = connection.getSocket();
+ BufferedSource source = Okio.buffer(Okio.source(socket));
+ BufferedSink sink = Okio.buffer(Okio.sink(socket));
+
+ final RealWebSocket webSocket =
+ new ConnectionWebSocket(response, connection, source, sink, random, listener);
+
+ // Start a dedicated thread for reading the web socket.
+ new Thread(new NamedRunnable("OkHttp WebSocket reader %s", request.urlString()) {
+ @Override protected void execute() {
+ while (webSocket.readMessage()) {
+ }
+ }
+ }).start();
+
+ // TODO connection.setOwner(webSocket);
+ Internal.instance.connectionSetOwner(connection, webSocket);
+
+ listener.onOpen(webSocket, request, response);
+ }
+
+ // Keep static so that the WebSocketCall instance can be garbage collected.
+ private static class ConnectionWebSocket extends RealWebSocket {
+ private final Connection connection;
+
+ public ConnectionWebSocket(Response response, Connection connection, BufferedSource source,
+ BufferedSink sink, Random random, WebSocketListener listener) {
+ super(true /* is client */, source, sink, random, listener, response.request().urlString());
+ this.connection = connection;
+ }
+
+ @Override protected void closeConnection() throws IOException {
+ // TODO connection.closeIfOwnedBy(this);
+ Internal.instance.closeIfOwnedBy(connection, this);
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketListener.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketListener.java
new file mode 100644
index 0000000..84f7cc0
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketListener.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import okio.Buffer;
+import okio.BufferedSource;
+
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType;
+
+// TODO move to public API!
+/** Listener for server-initiated messages on a connected {@link WebSocket}. */
+public interface WebSocketListener {
+ void onOpen(WebSocket webSocket, Request request, Response response) throws IOException;
+
+ /**
+ * Called when a server message is received. The {@code type} indicates whether the
+ * {@code payload} should be interpreted as UTF-8 text or binary data.
+ */
+ void onMessage(BufferedSource payload, PayloadType type) throws IOException;
+
+ /**
+ * Called when a server pong is received. This is usually a result of calling {@link
+ * WebSocket#sendPing(Buffer)} but might also be unsolicited.
+ */
+ void onPong(Buffer payload);
+
+ /**
+ * Called when the server sends a close message. This may have been initiated
+ * from a call to {@link WebSocket#close(int, String) close()} or as an unprompted
+ * message from the server.
+ *
+ * @param code The <a href="http://tools.ietf.org/html/rfc6455#section-7.4.1>RFC-compliant</a>
+ * status code.
+ * @param reason Reason for close or an empty string.
+ */
+ void onClose(int code, String reason);
+
+ /** Called when the transport or protocol layer of this web socket errors during communication. */
+ void onFailure(IOException e);
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
new file mode 100644
index 0000000..b4d17cc
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+public final class WebSocketProtocol {
+ /** Magic value which must be appended to the key in a response header. */
+ public static final String ACCEPT_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+ /*
+ Each frame starts with two bytes of data.
+
+ 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+ +-+-+-+-+-------+ +-+-------------+
+ |F|R|R|R| OP | |M| LENGTH |
+ |I|S|S|S| CODE | |A| |
+ |N|V|V|V| | |S| |
+ | |1|2|3| | |K| |
+ +-+-+-+-+-------+ +-+-------------+
+ */
+
+ /** Byte 0 flag for whether this is the final fragment in a message. */
+ static final int B0_FLAG_FIN = 0b10000000;
+ /** Byte 0 reserved flag 1. Must be 0 unless negotiated otherwise. */
+ static final int B0_FLAG_RSV1 = 0b01000000;
+ /** Byte 0 reserved flag 2. Must be 0 unless negotiated otherwise. */
+ static final int B0_FLAG_RSV2 = 0b00100000;
+ /** Byte 0 reserved flag 3. Must be 0 unless negotiated otherwise. */
+ static final int B0_FLAG_RSV3 = 0b00010000;
+ /** Byte 0 mask for the frame opcode. */
+ static final int B0_MASK_OPCODE = 0b00001111;
+ /** Flag in the opcode which indicates a control frame. */
+ static final int OPCODE_FLAG_CONTROL = 0b00001000;
+
+ /**
+ * Byte 1 flag for whether the payload data is masked.
+ * <p>
+ * If this flag is set, the next four bytes represent the mask key. These bytes appear after
+ * any additional bytes specified by {@link #B1_MASK_LENGTH}.
+ */
+ static final int B1_FLAG_MASK = 0b10000000;
+ /**
+ * Byte 1 mask for the payload length.
+ * <p>
+ * If this value is {@link #PAYLOAD_SHORT}, the next two bytes represent the length.
+ * If this value is {@link #PAYLOAD_LONG}, the next eight bytes represent the length.
+ */
+ static final int B1_MASK_LENGTH = 0b01111111;
+
+ static final int OPCODE_CONTINUATION = 0x0;
+ static final int OPCODE_TEXT = 0x1;
+ static final int OPCODE_BINARY = 0x2;
+
+ static final int OPCODE_CONTROL_CLOSE = 0x8;
+ static final int OPCODE_CONTROL_PING = 0x9;
+ 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}.
+ */
+ static final int PAYLOAD_MAX = 125;
+ /** Value for {@link #B1_MASK_LENGTH} which indicates the next two bytes are the length. */
+ static final int PAYLOAD_SHORT = 126;
+ /** Value for {@link #B1_MASK_LENGTH} which indicates the next eight bytes are the length. */
+ static final int PAYLOAD_LONG = 127;
+
+ static void toggleMask(byte[] buffer, long byteCount, byte[] key, long frameBytesRead) {
+ int keyLength = key.length;
+ for (int i = 0; i < byteCount; i++, frameBytesRead++) {
+ int keyIndex = (int) (frameBytesRead % keyLength);
+ buffer[i] = (byte) (buffer[i] ^ key[keyIndex]);
+ }
+ }
+
+ private WebSocketProtocol() {
+ throw new AssertionError("No instances.");
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
new file mode 100644
index 0000000..294854a
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.net.ProtocolException;
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+import okio.Timeout;
+
+import static com.squareup.okhttp.internal.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;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV3;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_MASK_OPCODE;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B1_FLAG_MASK;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B1_MASK_LENGTH;
+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_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_SHORT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
+import static java.lang.Integer.toHexString;
+
+/**
+ * An <a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a>-compatible WebSocket frame reader.
+ */
+public final class WebSocketReader {
+ public interface FrameCallback {
+ void onMessage(BufferedSource source, PayloadType type) throws IOException;
+ void onPing(Buffer buffer);
+ void onPong(Buffer buffer);
+ void onClose(int code, String reason) throws IOException;
+ }
+
+ private final boolean isClient;
+ private final BufferedSource source;
+ private final FrameCallback frameCallback;
+
+ private final Source framedMessageSource = new FramedMessageSource();
+
+ private boolean closed;
+ private boolean messageClosed;
+
+ // Stateful data about the current frame.
+ private int opcode;
+ private long frameLength;
+ private long frameBytesRead;
+ private boolean isFinalFrame;
+ private boolean isControlFrame;
+ private boolean isMasked;
+
+ private final byte[] maskKey = new byte[4];
+ private final byte[] maskBuffer = new byte[2048];
+
+ public WebSocketReader(boolean isClient, BufferedSource source, FrameCallback frameCallback) {
+ if (source == null) throw new NullPointerException("source");
+ if (frameCallback == null) throw new NullPointerException("frameCallback");
+ this.isClient = isClient;
+ this.source = source;
+ this.frameCallback = frameCallback;
+ }
+
+ /**
+ * Process the next protocol frame.
+ * <ul>
+ * <li>If it is a control frame this will result in a single call to {@link FrameCallback}.</li>
+ * <li>If it is a message frame this will result in a single call to {@link
+ * WebSocketListener#onMessage}. If the message spans multiple frames, each interleaved control
+ * frame will result in a corresponding call to {@link FrameCallback}.
+ * </ul>
+ */
+ public void processNextFrame() throws IOException {
+ readHeader();
+ if (isControlFrame) {
+ readControlFrame();
+ } else {
+ readMessageFrame();
+ }
+ }
+
+ private void readHeader() throws IOException {
+ if (closed) throw new IOException("Closed");
+
+ int b0 = source.readByte() & 0xff;
+
+ opcode = b0 & B0_MASK_OPCODE;
+ isFinalFrame = (b0 & B0_FLAG_FIN) != 0;
+ isControlFrame = (b0 & OPCODE_FLAG_CONTROL) != 0;
+
+ // Control frames must be final frames (cannot contain continuations).
+ if (isControlFrame && !isFinalFrame) {
+ throw new ProtocolException("Control frames must be final.");
+ }
+
+ boolean reservedFlag1 = (b0 & B0_FLAG_RSV1) != 0;
+ boolean reservedFlag2 = (b0 & B0_FLAG_RSV2) != 0;
+ boolean reservedFlag3 = (b0 & B0_FLAG_RSV3) != 0;
+ if (reservedFlag1 || reservedFlag2 || reservedFlag3) {
+ // Reserved flags are for extensions which we currently do not support.
+ throw new ProtocolException("Reserved flags are unsupported.");
+ }
+
+ int b1 = source.readByte() & 0xff;
+
+ isMasked = (b1 & B1_FLAG_MASK) != 0;
+ if (isMasked == isClient) {
+ // Masked payloads must be read on the server. Unmasked payloads must be read on the client.
+ throw new ProtocolException("Client-sent frames must be masked. Server sent must not.");
+ }
+
+ // Get frame length, optionally reading from follow-up bytes if indicated by special values.
+ frameLength = b1 & B1_MASK_LENGTH;
+ if (frameLength == PAYLOAD_SHORT) {
+ frameLength = source.readShort();
+ } else if (frameLength == PAYLOAD_LONG) {
+ frameLength = source.readLong();
+ }
+ frameBytesRead = 0;
+
+ if (isControlFrame && frameLength > PAYLOAD_MAX) {
+ throw new ProtocolException("Control frame must be less than " + PAYLOAD_MAX + "B.");
+ }
+
+ if (isMasked) {
+ // Read the masking key as bytes so that they can be used directly for unmasking.
+ source.readFully(maskKey);
+ }
+ }
+
+ private void readControlFrame() throws IOException {
+ Buffer buffer = null;
+ if (frameBytesRead < frameLength) {
+ buffer = new Buffer();
+
+ if (isClient) {
+ source.readFully(buffer, frameLength);
+ } else {
+ while (frameBytesRead < frameLength) {
+ int toRead = (int) Math.min(frameLength - frameBytesRead, maskBuffer.length);
+ int read = source.read(maskBuffer, 0, toRead);
+ if (read == -1) throw new EOFException();
+ toggleMask(maskBuffer, read, maskKey, frameBytesRead);
+ buffer.write(maskBuffer, 0, read);
+ frameBytesRead += read;
+ }
+ }
+ }
+
+ switch (opcode) {
+ case OPCODE_CONTROL_PING:
+ frameCallback.onPing(buffer);
+ break;
+ case OPCODE_CONTROL_PONG:
+ frameCallback.onPong(buffer);
+ break;
+ case OPCODE_CONTROL_CLOSE:
+ int code = 0;
+ String reason = "";
+ if (buffer != null) {
+ code = buffer.readShort();
+ reason = buffer.readUtf8();
+ }
+ frameCallback.onClose(code, reason);
+ closed = true;
+ break;
+ default:
+ throw new IllegalStateException("Unknown control opcode: " + toHexString(opcode));
+ }
+ }
+
+ private void readMessageFrame() throws IOException {
+ PayloadType type;
+ switch (opcode) {
+ case OPCODE_TEXT:
+ type = PayloadType.TEXT;
+ break;
+ case OPCODE_BINARY:
+ type = PayloadType.BINARY;
+ break;
+ default:
+ throw new IllegalStateException("Unknown opcode: " + toHexString(opcode));
+ }
+
+ messageClosed = false;
+ frameCallback.onMessage(Okio.buffer(framedMessageSource), type);
+ if (!messageClosed) {
+ throw new IllegalStateException("Listener failed to call close on message payload.");
+ }
+ }
+
+ /** Read headers and process any control frames until we reach a non-control frame. */
+ private void readUntilNonControlFrame() throws IOException {
+ while (!closed) {
+ readHeader();
+ if (!isControlFrame) {
+ break;
+ }
+ readControlFrame();
+ }
+ }
+
+ /**
+ * A special source which knows how to read a message body across one or more frames. Control
+ * frames that occur between fragments will be processed. If the message payload is masked this
+ * will unmask as it's being processed.
+ */
+ private final class FramedMessageSource implements Source {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (closed) throw new IOException("Closed");
+ if (messageClosed) throw new IllegalStateException("Closed");
+
+ if (frameBytesRead == frameLength) {
+ if (isFinalFrame) return -1; // We are exhausted and have no continuations.
+
+ readUntilNonControlFrame();
+ if (opcode != OPCODE_CONTINUATION) {
+ throw new ProtocolException("Expected continuation opcode. Got: " + toHexString(opcode));
+ }
+ if (isFinalFrame && frameLength == 0) {
+ return -1; // Fast-path for empty final frame.
+ }
+ }
+
+ long toRead = Math.min(byteCount, frameLength - frameBytesRead);
+
+ long read;
+ if (isMasked) {
+ toRead = Math.min(toRead, maskBuffer.length);
+ read = source.read(maskBuffer, 0, (int) toRead);
+ if (read == -1) throw new EOFException();
+ toggleMask(maskBuffer, read, maskKey, frameBytesRead);
+ sink.write(maskBuffer, 0, (int) read);
+ } else {
+ read = source.read(sink, toRead);
+ if (read == -1) throw new EOFException();
+ }
+
+ frameBytesRead += read;
+ return read;
+ }
+
+ @Override public Timeout timeout() {
+ return source.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ if (messageClosed) return;
+ messageClosed = true;
+ if (closed) return;
+
+ // Exhaust the remainder of the message, if any.
+ source.skip(frameLength - frameBytesRead);
+ while (!isFinalFrame) {
+ readUntilNonControlFrame();
+ source.skip(frameLength);
+ }
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
new file mode 100644
index 0000000..16d269b
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2014 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.ws;
+
+import java.io.IOException;
+import java.util.Random;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Timeout;
+
+import static com.squareup.okhttp.internal.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_SHORT;
+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.
+ */
+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;
+
+ private final FrameSink frameSink = new FrameSink();
+
+ private boolean closed;
+ private boolean activeWriter;
+
+ private final byte[] maskKey;
+ private final byte[] maskBuffer;
+
+ public WebSocketWriter(boolean isClient, BufferedSink sink, Random random) {
+ if (sink == null) throw new NullPointerException("sink");
+ if (random == null) throw new NullPointerException("random");
+ this.isClient = isClient;
+ this.sink = sink;
+ this.random = random;
+
+ // Masks are only a concern for client writers.
+ maskKey = isClient ? new byte[4] : null;
+ maskBuffer = isClient ? new byte[2048] : null;
+ }
+
+ /** 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);
+ }
+ }
+
+ /** 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);
+ }
+ }
+
+ /**
+ * Send a close frame with optional code and reason.
+ *
+ * @param code Status code as defined by
+ * <a href="http://tools.ietf.org/html/rfc6455#section-7.4">Section 7.4 of RFC 6455</a> or
+ * {@code 0}.
+ * @param reason Reason for shutting down or {@code null}. {@code code} is required if set.
+ */
+ public void writeClose(int code, String reason) throws IOException {
+ Buffer payload = null;
+ if (code != 0) {
+ if (code < 1000 || code >= 5000) {
+ throw new IllegalArgumentException("Code must be in range [1000,5000).");
+ }
+ payload = new Buffer();
+ payload.writeShort(code);
+ if (reason != null) {
+ payload.writeUtf8(reason);
+ }
+ } else if (reason != null) {
+ throw new IllegalArgumentException("Code required to include reason.");
+ }
+
+ synchronized (sink) {
+ writeControlFrame(OPCODE_CONTROL_CLOSE, payload);
+ closed = true;
+ }
+ }
+
+ private void writeControlFrame(int opcode, Buffer payload) throws IOException {
+ if (closed) throw new IOException("Closed");
+
+ int length = 0;
+ if (payload != null) {
+ length = (int) payload.size();
+ if (length > PAYLOAD_MAX) {
+ throw new IllegalArgumentException(
+ "Payload size must be less than or equal to " + PAYLOAD_MAX);
+ }
+ }
+
+ int b0 = B0_FLAG_FIN | opcode;
+ sink.writeByte(b0);
+
+ int b1 = length;
+ if (isClient) {
+ b1 |= B1_FLAG_MASK;
+ sink.writeByte(b1);
+
+ random.nextBytes(maskKey);
+ sink.write(maskKey);
+
+ if (payload != null) {
+ writeAllMasked(payload, length);
+ }
+ } else {
+ sink.writeByte(b1);
+
+ if (payload != null) {
+ sink.writeAll(payload);
+ }
+ }
+
+ sink.flush();
+ }
+
+ /**
+ * 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");
+ if (activeWriter) {
+ throw new IllegalStateException("Another message writer is active. Did you call close()?");
+ }
+ activeWriter = true;
+
+ frameSink.payloadType = type;
+ frameSink.isFirstFrame = true;
+ return Okio.buffer(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()?");
+ }
+ writeFrame(type, payload, payload.size(), true /* first frame */, true /* final */);
+ }
+
+ private void writeFrame(PayloadType payloadType, Buffer source, long byteCount,
+ boolean isFirstFrame, boolean isFinal) throws IOException {
+ if (closed) throw new IOException("Closed");
+
+ 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 <= Short.MAX_VALUE) {
+ 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);
+ int read = source.read(maskBuffer, 0, toRead);
+ if (read == -1) throw new AssertionError();
+ toggleMask(maskBuffer, read, maskKey, written);
+ sink.write(maskBuffer, 0, read);
+ written += read;
+ }
+ }
+
+ private final class FrameSink implements Sink {
+ private PayloadType payloadType;
+ private boolean isFirstFrame;
+
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ writeFrame(payloadType, source, byteCount, isFirstFrame, false /* final */);
+ isFirstFrame = false;
+ }
+
+ @Override public void flush() throws IOException {
+ if (closed) throw new IOException("Closed");
+
+ synchronized (sink) {
+ sink.flush();
+ }
+ }
+
+ @Override public Timeout timeout() {
+ return sink.timeout();
+ }
+
+ @SuppressWarnings("PointlessBitwiseExpression")
+ @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();
+ }
+
+ activeWriter = false;
+ }
+ }
+}
diff --git a/okio/CHANGELOG.md b/okio/CHANGELOG.md
new file mode 100644
index 0000000..eec5c2d
--- /dev/null
+++ b/okio/CHANGELOG.md
@@ -0,0 +1,83 @@
+Change Log
+==========
+
+## Version 1.2.0
+
+_2014-12-30_
+
+ * Fix: `Okio.buffer()` _always_ buffers for better predictability.
+ * Fix: Provide context when `readUtf8LineStrict()` throws.
+ * Fix: Buffers do not call through the `Source` on zero-byte writes.
+
+## Version 1.1.0
+
+_2014-12-11_
+ * Do UTF-8 encoding natively for a performance increase, particularly on Android.
+ * New APIs: `BufferedSink.emit()`, `BufferedSource.request()` and `BufferedSink.indexOfElement()`.
+ * Fixed a performance bug in `Buffer.indexOf()`
+
+## Version 1.0.1
+
+_2014-08-08_
+
+ * Added `read(byte[])`, `read(byte[], offset, byteCount)`, and
+ `void readFully(byte[])` to `BufferedSource`.
+ * Refined declared checked exceptions on `Buffer` methods.
+
+
+## Version 1.0.0
+
+_2014-05-23_
+
+ * Bumped release version. No other changes!
+
+## Version 0.9.0
+
+_2014-05-03_
+
+ * Use 0 as a sentinel for no timeout.
+ * Make AsyncTimeout public.
+ * Remove checked exception from Buffer.readByteArray.
+
+## Version 0.8.0
+
+_2014-04-24_
+
+ * Eagerly verify preconditions on public APIs.
+ * Quick return on Buffer instance equivalence.
+ * Add delegate types for Sink and Source.
+ * Small changes to the way deadlines are managed.
+ * Add append variant of Okio.sink for File.
+ * Methods to exhaust BufferedSource to byte[] and ByteString.
+
+## Version 0.7.0
+
+_2014-04-18_
+
+ * Don't use getters in timeout.
+ * Use the watchdog to interrupt sockets that have reached deadlines.
+ * Add java.io and java.nio file source/sink helpers.
+
+## Version 0.6.1
+
+_2014-04-17_
+
+ * Methods to read a buffered source fully in UTF-8 or supplied charset.
+ * API to read a byte[] directly.
+ * New methods to move all data from a source to a sink.
+ * Fix a bug on input stream exhaustion.
+
+## Version 0.6.0
+
+_2014-04-15_
+
+ * Make ByteString serializable.
+ * New API: `ByteString.of(byte[] data, int offset, int byteCount)`
+ * New API: stream-based copy, write, and read helpers.
+
+## Version 0.5.0
+
+_2014-04-08_
+
+ * Initial public release.
+ * Imported from OkHttp.
diff --git a/okio/CONTRIBUTING.md b/okio/CONTRIBUTING.md
new file mode 100644
index 0000000..c8b6ac1
--- /dev/null
+++ b/okio/CONTRIBUTING.md
@@ -0,0 +1,17 @@
+Contributing
+============
+
+If you would like to contribute code to Okio you can do so through GitHub by
+forking the repository and sending a pull request.
+
+When submitting code, please make every effort to follow existing conventions
+and style in order to keep the code as readable as possible. Please also make
+sure your code compiles by running `mvn clean verify`. Checkstyle failures
+during compilation indicate errors in your style and can be viewed in the
+`checkstyle-result.xml` file.
+
+Before your code can be accepted into the project you must also sign the
+[Individual Contributor License Agreement (CLA)][1].
+
+
+ [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1
diff --git a/okio/LICENSE.txt b/okio/LICENSE.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/okio/LICENSE.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/okio/MODULE_LICENSE_APACHE2 b/okio/MODULE_LICENSE_APACHE2
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/okio/MODULE_LICENSE_APACHE2
diff --git a/okio/README.android b/okio/README.android
new file mode 100644
index 0000000..a143b63
--- /dev/null
+++ b/okio/README.android
@@ -0,0 +1,12 @@
+URL: https://github.com/square/okio
+License: Apache 2
+Description: "A modern I/O API for Java"
+
+Local patches
+-------------
+
+All source changes (besides imports) marked with ANDROID-BEGIN and ANDROID-END:
+ - 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/okio/README.md b/okio/README.md
new file mode 100644
index 0000000..8aa1c5f
--- /dev/null
+++ b/okio/README.md
@@ -0,0 +1,142 @@
+Okio
+====
+
+Okio is a new library that complements `java.io` and `java.nio` to make it much
+easier to access, store, and process your data.
+
+ByteStrings and Buffers
+-----------------------
+
+Okio is built around two types that pack a lot of capability into a
+straightforward API:
+
+ * [**ByteString**][3] is an immutable sequence of bytes. For character data, `String`
+ is fundamental. `ByteString` is String's long-lost brother, making it easy to
+ treat binary data as a value. This class is ergonomic: it knows how to encode
+ and decode itself as hex, base64, and UTF-8.
+
+ * [**Buffer**][4] is a mutable sequence of bytes. Like `ArrayList`, you don't need
+ to size your buffer in advance. You read and write buffers as a queue: write
+ data to the end and read it from the front. There's no obligation to manage
+ positions, limits, or capacities.
+
+Internally, `ByteString` and `Buffer` do some clever things to save CPU and
+memory. If you encode a UTF-8 string as a `ByteString`, it caches a reference to
+that string so that if you decode it later, there's no work to do.
+
+`Buffer` is implemented as a linked list of segments. When you move data from
+one buffer to another, it _reassigns ownership_ of the segments rather than
+copying the data across. This approach is particularly helpful for multithreaded
+programs: a thread that talks to the network can exchange data with a worker
+thread without any copying or ceremony.
+
+Sources and Sinks
+-----------------
+
+An elegant part of the `java.io` design is how streams can be layered for
+transformations like encryption and compression. Okio includes its own stream
+types called [`Source`][5] and [`Sink`][6] that work like `InputStream` and
+`OutputStream`, but with some key differences:
+
+ * **Timeouts.** The streams provide access to the timeouts of the underlying
+ I/O mechanism. Unlike the `java.io` socket streams, both `read()` and
+ `write()` calls honor timeouts.
+
+ * **Easy to implement.** `Source` declares three methods: `read()`, `close()`,
+ and `timeout()`. There are no hazards like `available()` or single-byte reads
+ that cause correctness and performance surprises.
+
+ * **Easy to use.** Although _implementations_ of `Source` and `Sink` have only
+ three methods to write, _callers_ are given a rich API with the
+ [`BufferedSource`][7] and [`BufferedSink`][8] interfaces. These interfaces give you
+ everything you need in one place.
+
+ * **No artificial distinction between byte streams and char streams.** It's all
+ data. Read and write it as bytes, UTF-8 strings, big-endian 32-bit integers,
+ little-endian shorts; whatever you want. No more `InputStreamReader`!
+
+ * **Easy to test.** The `Buffer` class implements both `BufferedSource` and
+ `BufferedSink` so your test code is simple and clear.
+
+Sources and sinks interoperate with `InputStream` and `OutputStream`. You can
+view any `Source` as an `InputStream`, and you can view any `InputStream` as a
+`Source`. Similarly for `Sink` and `OutputStream`.
+
+Dependable
+----------
+
+Okio started as a component of [OkHttp][1], the capable HTTP+SPDY client
+included in Android. It's well-exercised and ready to solve new problems.
+
+
+Example: a PNG decoder
+----------------------
+
+Decoding the chunks of a PNG file demonstrates Okio in practice.
+
+```java
+private static final ByteString PNG_HEADER = ByteString.decodeHex("89504e470d0a1a0a");
+
+public void decodePng(InputStream in) throws IOException {
+ BufferedSource pngSource = Okio.buffer(Okio.source(in));
+
+ ByteString header = pngSource.readByteString(PNG_HEADER.size());
+ if (!header.equals(PNG_HEADER)) {
+ throw new IOException("Not a PNG.");
+ }
+
+ while (true) {
+ Buffer chunk = new Buffer();
+
+ // Each chunk is a length, type, data, and CRC offset.
+ int length = pngSource.readInt();
+ String type = pngSource.readUtf8(4);
+ pngSource.readFully(chunk, length);
+ int crc = pngSource.readInt();
+
+ decodeChunk(type, chunk);
+ if (type.equals("IEND")) break;
+ }
+
+ pngSource.close();
+}
+
+private void decodeChunk(String type, Buffer chunk) {
+ if (type.equals("IHDR")) {
+ int width = chunk.readInt();
+ int height = chunk.readInt();
+ System.out.printf("%08x: %s %d x %d%n", chunk.size(), type, width, height);
+ } else {
+ System.out.printf("%08x: %s%n", chunk.size(), type);
+ }
+}
+```
+
+Download
+--------
+
+Download [the latest JAR][2] or grab via Maven:
+```xml
+<dependency>
+ <groupId>com.squareup.okio</groupId>
+ <artifactId>okio</artifactId>
+ <version>1.1.0</version>
+</dependency>
+```
+or Gradle:
+```groovy
+compile 'com.squareup.okio:okio:1.1.0'
+```
+
+Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
+
+
+ [1]: https://github.com/square/okhttp
+ [2]: https://search.maven.org/remote_content?g=com.squareup.okio&a=okio&v=LATEST
+ [3]: http://square.github.io/okio/okio/ByteString.html
+ [4]: http://square.github.io/okio/okio/Buffer.html
+ [5]: http://square.github.io/okio/okio/Source.html
+ [6]: http://square.github.io/okio/okio/Sink.html
+ [7]: http://square.github.io/okio/okio/BufferedSource.html
+ [8]: http://square.github.io/okio/okio/BufferedSink.html
+ [snap]: https://oss.sonatype.org/content/repositories/snapshots/
diff --git a/okio/benchmarks/README.md b/okio/benchmarks/README.md
new file mode 100644
index 0000000..637f13e
--- /dev/null
+++ b/okio/benchmarks/README.md
@@ -0,0 +1,38 @@
+Okio Benchmarks
+------------
+
+This module contains microbenchmarks that can be used to measure various aspects of performance for Okio buffers. Okio benchmarks are written using JMH (version 1.4.1 at this time) and require Java 7.
+
+Running Locally
+-------------
+
+To run benchmarks locally, first build and package the project modules:
+
+```
+$ mvn clean package
+```
+
+This should create a `benchmarks.jar` file in the `target` directory, which is a typical JMH benchmark JAR:
+
+```
+$ java -jar benchmarks/target/benchmarks.jar -l
+Benchmarks:
+com.squareup.okio.benchmarks.BufferPerformanceBench.cold
+com.squareup.okio.benchmarks.BufferPerformanceBench.threads16hot
+com.squareup.okio.benchmarks.BufferPerformanceBench.threads1hot
+com.squareup.okio.benchmarks.BufferPerformanceBench.threads2hot
+com.squareup.okio.benchmarks.BufferPerformanceBench.threads32hot
+com.squareup.okio.benchmarks.BufferPerformanceBench.threads4hot
+com.squareup.okio.benchmarks.BufferPerformanceBench.threads8hot
+```
+
+More help is available using the `-h` option. A typical run on Mac OS X looks like:
+
+```
+$ /usr/libexec/java_home -v 1.7 --exec java -jar benchmarks/target/benchmarks.jar \
+"cold" -prof gc,hs_rt,stack -r 60 -t 4 \
+-jvmArgsPrepend "-Xms1G -Xmx1G -XX:+HeapDumpOnOutOfMemoryError"
+```
+
+This executes the "cold" buffer usage benchmark, using the default number of measurement and warm-up iterations, forks, and threads; it adjusts the thread count to 4, iteration time to 60 seconds, fixes the heap size at 1GB and profiles the benchmark using JMH's GC, Hotspot runtime and stack sampling profilers.
+
diff --git a/okio/benchmarks/pom.xml b/okio/benchmarks/pom.xml
new file mode 100644
index 0000000..429c857
--- /dev/null
+++ b/okio/benchmarks/pom.xml
@@ -0,0 +1,82 @@
+<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/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>okio-parent</artifactId>
+ <groupId>com.squareup.okio</groupId>
+ <version>1.3.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>benchmarks</artifactId>
+ <packaging>jar</packaging>
+
+ <name>Okio Performance Benchmarks</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.openjdk.jmh</groupId>
+ <artifactId>jmh-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.openjdk.jmh</groupId>
+ <artifactId>jmh-generator-annprocess</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okio</groupId>
+ <artifactId>okio</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ </dependencies>
+
+ <properties>
+ <uberjar.name>benchmarks</uberjar.name>
+ </properties>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>2.2</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <finalName>${uberjar.name}</finalName>
+ <transformers>
+ <transformer
+ implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>org.openjdk.jmh.Main</mainClass>
+ </transformer>
+ </transformers>
+ <filters>
+ <filter>
+ <!--
+ Shading signed JARs will fail without this.
+ http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
+ -->
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+
+</project>
diff --git a/okio/benchmarks/src/main/java/com/squareup/okio/benchmarks/BufferPerformanceBench.java b/okio/benchmarks/src/main/java/com/squareup/okio/benchmarks/BufferPerformanceBench.java
new file mode 100644
index 0000000..1bb7f58
--- /dev/null
+++ b/okio/benchmarks/src/main/java/com/squareup/okio/benchmarks/BufferPerformanceBench.java
@@ -0,0 +1,330 @@
+/*
+ * Copyright (C) 2014 Square, Inc. and others.
+ *
+ * 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.okio.benchmarks;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Group;
+import org.openjdk.jmh.annotations.GroupThreads;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.TearDown;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import okio.Buffer;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Sink;
+import okio.Timeout;
+
+import static java.util.Objects.requireNonNull;
+
+@Fork(1)
+@Warmup(iterations = 10, time = 10)
+@Measurement(iterations = 10, time = 10)
+@State(Scope.Benchmark)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.SECONDS)
+public class BufferPerformanceBench {
+
+ public static final File OriginPath =
+ new File(System.getProperty("okio.bench.origin.path", "/dev/urandom"));
+
+ /* Test Workload
+ *
+ * Each benchmark thread maintains three buffers; a receive buffer, a process buffer
+ * and a send buffer. At every operation:
+ *
+ * - We fill up the receive buffer using the origin, write the request to the process
+ * buffer, and consume the process buffer.
+ * - We fill up the process buffer using the origin, write the response to the send
+ * buffer, and consume the send buffer.
+ *
+ * We use an "origin" source that serves as a preexisting sequence of bytes we can read
+ * from the file system. The request and response bytes are initialized in the beginning
+ * and reused throughout the benchmark in order to eliminate GC effects.
+ *
+ * Typically, we simulate the usage of small reads and large writes. Requests and
+ * responses are satisfied with precomputed buffers to eliminate GC effects on
+ * results.
+ *
+ * There are two types of benchmark tests; hot tests are "pedal to the metal" and
+ * use all CPU they can take. These are useful to magnify performance effects of
+ * changes but are not realistic use cases that should drive optimization efforts.
+ * Cold tests introduce think time between the receiving of the request and sending
+ * of the response. They are more useful as a reasonably realistic workload where
+ * buffers can be read from and written to during request/response handling but
+ * may hide subtle effects of most changes on performance. Prefer to look at the cold
+ * benchmarks first to decide if a bottleneck is worth pursuing, then use the hot
+ * benchmarks to fine tune optimization efforts.
+ *
+ * Benchmark threads do not explicitly communicate between each other (except to sync
+ * iterations as needed by JMH).
+ *
+ * We simulate think time for each benchmark thread by parking the thread for a
+ * configurable number of microseconds (1000 by default).
+ */
+
+
+ @Benchmark
+ @Threads(1)
+ public void threads1hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(2)
+ public void threads2hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(4)
+ public void threads4hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(8)
+ public void threads8hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(16)
+ public void threads16hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @Threads(32)
+ public void threads32hot(HotBuffers buffers) throws IOException {
+ readWriteRecycle(buffers);
+ }
+
+ @Benchmark
+ @GroupThreads(1)
+ @Group("cold")
+ public void thinkReadHot(HotBuffers buffers) throws IOException {
+ buffers.receive(requestBytes).readAll(NullSink);
+ }
+
+ @Benchmark
+ @GroupThreads(3)
+ @Group("cold")
+ public void thinkWriteCold(ColdBuffers buffers) throws IOException {
+ buffers.transmit(responseBytes).readAll(NullSink);
+ }
+
+ private void readWriteRecycle(HotBuffers buffers) throws IOException {
+ buffers.receive(requestBytes).readAll(NullSink);
+ buffers.transmit(responseBytes).readAll(NullSink);
+ }
+
+ @Param({ "1000" })
+ int maxThinkMicros = 1000;
+
+ @Param({ "1024" })
+ int maxReadBytes = 1024;
+
+ @Param({ "1024" })
+ int maxWriteBytes = 1024;
+
+ @Param({ "2048" })
+ int requestSize = 2048;
+
+ @Param({ "1" })
+ int responseFactor = 1;
+
+ byte[] requestBytes;
+
+ byte[] responseBytes;
+
+ @Setup(Level.Trial)
+ public void storeRequestResponseData() throws IOException {
+ checkOrigin(OriginPath);
+
+ requestBytes = storeSourceData(new byte[requestSize]);
+ responseBytes = storeSourceData(new byte[requestSize * responseFactor]);
+ }
+
+ private byte[] storeSourceData(byte[] dest) throws IOException {
+ requireNonNull(dest, "dest == null");
+ try (BufferedSource source = Okio.buffer(Okio.source(OriginPath))) {
+ source.readFully(dest);
+ }
+ return dest;
+ }
+
+ private void checkOrigin(File path) throws IOException {
+ requireNonNull(path, "path == null");
+
+ if (!path.canRead()) {
+ throw new IllegalArgumentException("can not access: " + path);
+ }
+
+ try (InputStream in = new FileInputStream(path)) {
+ int available = in.read();
+ if (available < 0) {
+ throw new IllegalArgumentException("can not read: " + path);
+ }
+ }
+ }
+
+ /*
+ * The state class hierarchy is larger than it needs to be due to a JMH
+ * issue where states inheriting setup methods depending on another state
+ * do not get initialized correctly from benchmark methods making use
+ * of groups. To work around, we leave the common setup and teardown code
+ * in superclasses and move the setup method depending on the bench state
+ * to subclasses. Without the workaround, it would have been enough for
+ * `ColdBuffers` to inherit from `HotBuffers`.
+ */
+
+ @State(Scope.Thread)
+ public static class ColdBuffers extends BufferSetup {
+
+ @Setup(Level.Trial)
+ public void setupBench(BufferPerformanceBench bench) {
+ super.bench = bench;
+ }
+
+ @Setup(Level.Invocation)
+ public void lag() throws InterruptedException {
+ TimeUnit.MICROSECONDS.sleep(bench.maxThinkMicros);
+ }
+
+ }
+
+ @State(Scope.Thread)
+ public static class HotBuffers extends BufferSetup {
+
+ @Setup(Level.Trial)
+ public void setupBench(BufferPerformanceBench bench) {
+ super.bench = bench;
+ }
+
+ }
+
+ @State(Scope.Thread)
+ public static abstract class BufferSetup extends BufferState {
+ BufferPerformanceBench bench;
+
+ public BufferedSource receive(byte[] bytes) throws IOException {
+ return super.receive(bytes, bench.maxReadBytes);
+ }
+
+ public BufferedSource transmit(byte[] bytes) throws IOException {
+ return super.transmit(bytes, bench.maxWriteBytes);
+ }
+
+ @TearDown
+ public void dispose() throws IOException {
+ releaseBuffers();
+ }
+
+ }
+
+ public static class BufferState {
+
+ @SuppressWarnings("resource")
+ final Buffer received = new Buffer();
+ @SuppressWarnings("resource")
+ final Buffer sent = new Buffer();
+ @SuppressWarnings("resource")
+ final Buffer process = new Buffer();
+
+ public void releaseBuffers() throws IOException {
+ received.clear();
+ sent.clear();
+ process.clear();
+ }
+
+ /**
+ * Fills up the receive buffer, hands off to process buffer and returns it for consuming.
+ * Expects receive and process buffers to be empty. Leaves the receive buffer empty and
+ * process buffer full.
+ */
+ protected Buffer receive(byte[] bytes, int maxChunkSize) throws IOException {
+ writeChunked(received, bytes, maxChunkSize).readAll(process);
+ return process;
+ }
+
+ /**
+ * Fills up the process buffer, hands off to send buffer and returns it for consuming.
+ * Expects process and sent buffers to be empty. Leaves the process buffer empty and
+ * sent buffer full.
+ */
+ protected BufferedSource transmit(byte[] bytes, int maxChunkSize) throws IOException {
+ writeChunked(process, bytes, maxChunkSize).readAll(sent);
+ return sent;
+ }
+
+ private BufferedSource writeChunked(Buffer buffer, byte[] bytes, final int chunkSize) {
+ int remaining = bytes.length;
+ int offset = 0;
+ while (remaining > 0) {
+ int bytesToWrite = Math.min(remaining, chunkSize);
+ buffer.write(bytes, offset, bytesToWrite);
+ remaining -= bytesToWrite;
+ offset += bytesToWrite;
+ }
+ return buffer;
+ }
+
+ }
+
+ @SuppressWarnings("resource")
+ private static final Sink NullSink = new Sink() {
+
+ @Override public void write(Buffer source, long byteCount) throws EOFException {
+ source.skip(byteCount);
+ }
+
+ @Override public void flush() {
+ // nothing
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() {
+ // nothing
+ }
+
+ @Override public String toString() {
+ return "NullSink{}";
+ }
+ };
+
+}
diff --git a/okio/checkstyle.xml b/okio/checkstyle.xml
new file mode 100644
index 0000000..794af42
--- /dev/null
+++ b/okio/checkstyle.xml
@@ -0,0 +1,131 @@
+<?xml version="1.0"?>
+<!DOCTYPE module PUBLIC
+ "-//Puppy Crawl//DTD Check Configuration 1.2//EN"
+ "http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
+
+<module name="Checker">
+ <module name="NewlineAtEndOfFile"/>
+ <module name="FileLength"/>
+ <module name="FileTabCharacter"/>
+
+ <!-- Trailing spaces -->
+ <module name="RegexpSingleline">
+ <property name="format" value="\s+$"/>
+ <property name="message" value="Line has trailing spaces."/>
+ </module>
+
+ <!-- Space after 'for' and 'if' -->
+ <module name="RegexpSingleline">
+ <property name="format" value="^\s*(for|if)\b[^ ]"/>
+ <property name="message" value="Space needed before opening parenthesis."/>
+ </module>
+
+ <!-- For each spacing -->
+ <module name="RegexpSingleline">
+ <property name="format" value="^\s*for \(.*?([^ ]:|:[^ ])"/>
+ <property name="message" value="Space needed around ':' character."/>
+ </module>
+
+ <module name="TreeWalker">
+ <property name="cacheFile" value="${checkstyle.cache.file}"/>
+
+ <!-- Checks for Javadoc comments. -->
+ <!-- See http://checkstyle.sf.net/config_javadoc.html -->
+ <!--module name="JavadocMethod"/-->
+ <!--module name="JavadocType"/-->
+ <!--module name="JavadocVariable"/-->
+ <module name="JavadocStyle"/>
+
+
+ <!-- Checks for Naming Conventions. -->
+ <!-- See http://checkstyle.sf.net/config_naming.html -->
+ <!--<module name="ConstantName"/>-->
+ <module name="LocalFinalVariableName"/>
+ <module name="LocalVariableName"/>
+ <module name="MemberName"/>
+ <module name="MethodName"/>
+ <module name="PackageName"/>
+ <module name="ParameterName"/>
+ <module name="StaticVariableName"/>
+ <module name="TypeName"/>
+
+
+ <!-- Checks for imports -->
+ <!-- See http://checkstyle.sf.net/config_import.html -->
+ <module name="AvoidStarImport"/>
+ <module name="IllegalImport"/>
+ <!-- defaults to sun.* packages -->
+ <module name="RedundantImport"/>
+ <module name="UnusedImports"/>
+
+
+ <!-- Checks for Size Violations. -->
+ <!-- See http://checkstyle.sf.net/config_sizes.html -->
+ <module name="LineLength">
+ <property name="max" value="100"/>
+ </module>
+ <module name="MethodLength"/>
+
+
+ <!-- Checks for whitespace -->
+ <!-- See http://checkstyle.sf.net/config_whitespace.html -->
+ <module name="GenericWhitespace"/>
+ <!--<module name="EmptyForIteratorPad"/>-->
+ <module name="MethodParamPad"/>
+ <!--<module name="NoWhitespaceAfter"/>-->
+ <!--<module name="NoWhitespaceBefore"/>-->
+ <module name="OperatorWrap"/>
+ <module name="ParenPad"/>
+ <module name="TypecastParenPad"/>
+ <module name="WhitespaceAfter"/>
+ <module name="WhitespaceAround"/>
+
+
+ <!-- Modifier Checks -->
+ <!-- See http://checkstyle.sf.net/config_modifiers.html -->
+ <module name="ModifierOrder"/>
+ <module name="RedundantModifier"/>
+
+
+ <!-- Checks for blocks. You know, those {}'s -->
+ <!-- See http://checkstyle.sf.net/config_blocks.html -->
+ <module name="AvoidNestedBlocks"/>
+ <!--module name="EmptyBlock"/-->
+ <module name="LeftCurly"/>
+ <!--<module name="NeedBraces"/>-->
+ <module name="RightCurly"/>
+
+
+ <!-- Checks for common coding problems -->
+ <!-- See http://checkstyle.sf.net/config_coding.html -->
+ <!--module name="AvoidInlineConditionals"/-->
+ <module name="CovariantEquals"/>
+ <module name="EmptyStatement"/>
+ <!--<module name="EqualsAvoidNull"/>-->
+ <module name="EqualsHashCode"/>
+ <!--module name="HiddenField"/-->
+ <module name="IllegalInstantiation"/>
+ <!--module name="InnerAssignment"/-->
+ <!--module name="MagicNumber"/-->
+ <!--module name="MissingSwitchDefault"/-->
+ <module name="RedundantThrows"/>
+ <module name="SimplifyBooleanExpression"/>
+ <module name="SimplifyBooleanReturn"/>
+
+ <!-- Checks for class design -->
+ <!-- See http://checkstyle.sf.net/config_design.html -->
+ <!--module name="DesignForExtension"/-->
+ <!--<module name="FinalClass"/>-->
+ <module name="HideUtilityClassConstructor"/>
+ <module name="InterfaceIsType"/>
+ <!--module name="VisibilityModifier"/-->
+
+
+ <!-- Miscellaneous other checks. -->
+ <!-- See http://checkstyle.sf.net/config_misc.html -->
+ <module name="ArrayTypeStyle"/>
+ <!--module name="FinalParameters"/-->
+ <!--module name="TodoComment"/-->
+ <module name="UpperEll"/>
+ </module>
+</module>
diff --git a/okio/deploy_javadoc.sh b/okio/deploy_javadoc.sh
new file mode 100755
index 0000000..0164301
--- /dev/null
+++ b/okio/deploy_javadoc.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+set -ex
+
+REPO="git@github.com:square/okio.git"
+GROUP_ID="com.squareup.okio"
+ARTIFACT_ID="okio"
+
+DIR=temp-clone
+
+# Delete any existing temporary website clone
+rm -rf $DIR
+
+# Clone the current repo into temp folder
+git clone $REPO $DIR
+
+# Move working directory into temp folder
+cd $DIR
+
+# Checkout and track the gh-pages branch
+git checkout -t origin/gh-pages
+
+# Delete everything
+rm -rf *
+
+# Download the latest javadoc
+curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$ARTIFACT_ID&v=LATEST&c=javadoc" > javadoc.zip
+unzip javadoc.zip
+rm javadoc.zip
+
+# Stage all files in git and create a commit
+git add .
+git add -u
+git commit -m "Website at $(date)"
+
+# Push the new files up to GitHub
+git push origin gh-pages
+
+# Delete our temp folder
+cd ..
+rm -rf $DIR
diff --git a/okio/okio/pom.xml b/okio/okio/pom.xml
new file mode 100644
index 0000000..a560ee2
--- /dev/null
+++ b/okio/okio/pom.xml
@@ -0,0 +1,53 @@
+<?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.okio</groupId>
+ <artifactId>okio-parent</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>okio</artifactId>
+ <name>Okio</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-annotations</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-maven-plugin</artifactId>
+ <version>${animal.sniffer.version}</version>
+ <executions>
+ <execution>
+ <phase>test</phase>
+ <goals>
+ <goal>check</goal>
+ </goals>
+ </execution>
+ </executions>
+ <configuration>
+ <signature>
+ <groupId>org.codehaus.mojo.signature</groupId>
+ <artifactId>java16</artifactId>
+ <version>1.1</version>
+ </signature>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/okio/okio/src/main/java/okio/AsyncTimeout.java b/okio/okio/src/main/java/okio/AsyncTimeout.java
new file mode 100644
index 0000000..cda6ff4
--- /dev/null
+++ b/okio/okio/src/main/java/okio/AsyncTimeout.java
@@ -0,0 +1,318 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+
+/**
+ * This timeout uses a background thread to take action exactly when the timeout
+ * occurs. Use this to implement timeouts where they aren't supported natively,
+ * such as to sockets that are blocked on writing.
+ *
+ * <p>Subclasses should override {@link #timedOut} to take action when a timeout
+ * occurs. This method will be invoked by the shared watchdog thread so it
+ * should not do any long-running operations. Otherwise we risk starving other
+ * timeouts from being triggered.
+ *
+ * <p>Use {@link #sink} and {@link #source} to apply this timeout to a stream.
+ * The returned value will apply the timeout to each operation on the wrapped
+ * stream.
+ *
+ * <p>Callers should call {@link #enter} before doing work that is subject to
+ * timeouts, and {@link #exit} afterwards. The return value of {@link #exit}
+ * indicates whether a timeout was triggered. Note that the call to {@link
+ * #timedOut} is asynchronous, and may be called after {@link #exit}.
+ */
+public class AsyncTimeout extends Timeout {
+ /**
+ * The watchdog thread processes a linked list of pending timeouts, sorted in
+ * the order to be triggered. This class synchronizes on AsyncTimeout.class.
+ * This lock guards the queue.
+ *
+ * <p>Head's 'next' points to the first element of the linked list. The first
+ * element is the next node to time out, or null if the queue is empty. The
+ * head is null until the watchdog thread is started.
+ */
+ private static AsyncTimeout head;
+
+ /** True if this node is currently in the queue. */
+ private boolean inQueue;
+
+ /** The next node in the linked list. */
+ private AsyncTimeout next;
+
+ /** If scheduled, this is the time that the watchdog should time this out. */
+ private long timeoutAt;
+
+ public final void enter() {
+ if (inQueue) throw new IllegalStateException("Unbalanced enter/exit");
+ long timeoutNanos = timeoutNanos();
+ boolean hasDeadline = hasDeadline();
+ if (timeoutNanos == 0 && !hasDeadline) {
+ return; // No timeout and no deadline? Don't bother with the queue.
+ }
+ inQueue = true;
+ scheduleTimeout(this, timeoutNanos, hasDeadline);
+ }
+
+ private static synchronized void scheduleTimeout(
+ AsyncTimeout node, long timeoutNanos, boolean hasDeadline) {
+ // Start the watchdog thread and create the head node when the first timeout is scheduled.
+ if (head == null) {
+ head = new AsyncTimeout();
+ new Watchdog().start();
+ }
+
+ long now = System.nanoTime();
+ if (timeoutNanos != 0 && hasDeadline) {
+ // Compute the earliest event; either timeout or deadline. Because nanoTime can wrap around,
+ // Math.min() is undefined for absolute values, but meaningful for relative ones.
+ node.timeoutAt = now + Math.min(timeoutNanos, node.deadlineNanoTime() - now);
+ } else if (timeoutNanos != 0) {
+ node.timeoutAt = now + timeoutNanos;
+ } else if (hasDeadline) {
+ node.timeoutAt = node.deadlineNanoTime();
+ } else {
+ throw new AssertionError();
+ }
+
+ // Insert the node in sorted order.
+ long remainingNanos = node.remainingNanos(now);
+ for (AsyncTimeout prev = head; true; prev = prev.next) {
+ if (prev.next == null || remainingNanos < prev.next.remainingNanos(now)) {
+ node.next = prev.next;
+ prev.next = node;
+ if (prev == head) {
+ AsyncTimeout.class.notify(); // Wake up the watchdog when inserting at the front.
+ }
+ break;
+ }
+ }
+ }
+
+ /** Returns true if the timeout occurred. */
+ public final boolean exit() {
+ if (!inQueue) return false;
+ inQueue = false;
+ return cancelScheduledTimeout(this);
+ }
+
+ /** Returns true if the timeout occurred. */
+ private static synchronized boolean cancelScheduledTimeout(AsyncTimeout node) {
+ // Remove the node from the linked list.
+ for (AsyncTimeout prev = head; prev != null; prev = prev.next) {
+ if (prev.next == node) {
+ prev.next = node.next;
+ node.next = null;
+ return false;
+ }
+ }
+
+ // The node wasn't found in the linked list: it must have timed out!
+ return true;
+ }
+
+ /**
+ * Returns the amount of time left until the time out. This will be negative
+ * if the timeout has elapsed and the timeout should occur immediately.
+ */
+ private long remainingNanos(long now) {
+ return timeoutAt - now;
+ }
+
+ /**
+ * Invoked by the watchdog thread when the time between calls to {@link
+ * #enter()} and {@link #exit()} has exceeded the timeout.
+ */
+ protected void timedOut() {
+ }
+
+ /**
+ * Returns a new sink that delegates to {@code sink}, using this to implement
+ * timeouts. This works best if {@link #timedOut} is overridden to interrupt
+ * {@code sink}'s current operation.
+ */
+ public final Sink sink(final Sink sink) {
+ return new Sink() {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ boolean throwOnTimeout = false;
+ enter();
+ try {
+ sink.write(source, byteCount);
+ throwOnTimeout = true;
+ } catch (IOException e) {
+ throw exit(e);
+ } finally {
+ exit(throwOnTimeout);
+ }
+ }
+
+ @Override public void flush() throws IOException {
+ boolean throwOnTimeout = false;
+ enter();
+ try {
+ sink.flush();
+ throwOnTimeout = true;
+ } catch (IOException e) {
+ throw exit(e);
+ } finally {
+ exit(throwOnTimeout);
+ }
+ }
+
+ @Override public void close() throws IOException {
+ boolean throwOnTimeout = false;
+ enter();
+ try {
+ sink.close();
+ throwOnTimeout = true;
+ } catch (IOException e) {
+ throw exit(e);
+ } finally {
+ exit(throwOnTimeout);
+ }
+ }
+
+ @Override public Timeout timeout() {
+ return AsyncTimeout.this;
+ }
+
+ @Override public String toString() {
+ return "AsyncTimeout.sink(" + sink + ")";
+ }
+ };
+ }
+
+ /**
+ * Returns a new source that delegates to {@code source}, using this to
+ * implement timeouts. This works best if {@link #timedOut} is overridden to
+ * interrupt {@code sink}'s current operation.
+ */
+ public final Source source(final Source source) {
+ return new Source() {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ boolean throwOnTimeout = false;
+ enter();
+ try {
+ long result = source.read(sink, byteCount);
+ throwOnTimeout = true;
+ return result;
+ } catch (IOException e) {
+ throw exit(e);
+ } finally {
+ exit(throwOnTimeout);
+ }
+ }
+
+ @Override public void close() throws IOException {
+ boolean throwOnTimeout = false;
+ try {
+ source.close();
+ throwOnTimeout = true;
+ } catch (IOException e) {
+ throw exit(e);
+ } finally {
+ exit(throwOnTimeout);
+ }
+ }
+
+ @Override public Timeout timeout() {
+ return AsyncTimeout.this;
+ }
+
+ @Override public String toString() {
+ return "AsyncTimeout.source(" + source + ")";
+ }
+ };
+ }
+
+ /**
+ * Throws an InterruptedIOException if {@code throwOnTimeout} is true and a
+ * timeout occurred.
+ */
+ final void exit(boolean throwOnTimeout) throws IOException {
+ boolean timedOut = exit();
+ if (timedOut && throwOnTimeout) throw new InterruptedIOException("timeout");
+ }
+
+ /**
+ * Returns either {@code cause} or an InterruptedIOException that's caused by
+ * {@code cause} if a timeout occurred.
+ */
+ final IOException exit(IOException cause) throws IOException {
+ if (!exit()) return cause;
+ InterruptedIOException e = new InterruptedIOException("timeout");
+ e.initCause(cause);
+ return e;
+ }
+
+ private static final class Watchdog extends Thread {
+ public Watchdog() {
+ super("Okio Watchdog");
+ setDaemon(true);
+ }
+
+ public void run() {
+ while (true) {
+ try {
+ AsyncTimeout timedOut = awaitTimeout();
+
+ // Didn't find a node to interrupt. Try again.
+ if (timedOut == null) continue;
+
+ // Close the timed out node.
+ timedOut.timedOut();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Removes and returns the node at the head of the list, waiting for it to
+ * time out if necessary. Returns null if the situation changes while waiting:
+ * either a newer node is inserted at the head, or the node being waited on
+ * has been removed.
+ */
+ private static synchronized AsyncTimeout awaitTimeout() throws InterruptedException {
+ // Get the next eligible node.
+ AsyncTimeout node = head.next;
+
+ // The queue is empty. Wait for something to be enqueued.
+ if (node == null) {
+ AsyncTimeout.class.wait();
+ return null;
+ }
+
+ long waitNanos = node.remainingNanos(System.nanoTime());
+
+ // The head of the queue hasn't timed out yet. Await that.
+ if (waitNanos > 0) {
+ // Waiting is made complicated by the fact that we work in nanoseconds,
+ // but the API wants (millis, nanos) in two arguments.
+ long waitMillis = waitNanos / 1000000L;
+ waitNanos -= (waitMillis * 1000000L);
+ AsyncTimeout.class.wait(waitMillis, (int) waitNanos);
+ return null;
+ }
+
+ // The head of the queue has timed out. Remove it.
+ head.next = node.next;
+ node.next = null;
+ return node;
+ }
+}
diff --git a/okio/src/main/java/okio/Base64.java b/okio/okio/src/main/java/okio/Base64.java
similarity index 99%
rename from okio/src/main/java/okio/Base64.java
rename to okio/okio/src/main/java/okio/Base64.java
index 087b287..dac78bd 100644
--- a/okio/src/main/java/okio/Base64.java
+++ b/okio/okio/src/main/java/okio/Base64.java
@@ -18,7 +18,6 @@
/**
* @author Alexander Y. Kleymenov
*/
-
package okio;
import java.io.UnsupportedEncodingException;
diff --git a/okio/okio/src/main/java/okio/Buffer.java b/okio/okio/src/main/java/okio/Buffer.java
new file mode 100644
index 0000000..3bd1175
--- /dev/null
+++ b/okio/okio/src/main/java/okio/Buffer.java
@@ -0,0 +1,1060 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static okio.Util.checkOffsetAndCount;
+import static okio.Util.reverseBytesLong;
+
+/**
+ * A collection of bytes in memory.
+ *
+ * <p><strong>Moving data from one buffer to another is fast.</strong> Instead
+ * of copying bytes from one place in memory to another, this class just changes
+ * ownership of the underlying byte arrays.
+ *
+ * <p><strong>This buffer grows with your data.</strong> Just like ArrayList,
+ * each buffer starts small. It consumes only the memory it needs to.
+ *
+ * <p><strong>This buffer pools its byte arrays.</strong> When you allocate a
+ * byte array in Java, the runtime must zero-fill the requested array before
+ * returning it to you. Even if you're going to write over that space anyway.
+ * This class avoids zero-fill and GC churn by pooling byte arrays.
+ */
+public final class Buffer implements BufferedSource, BufferedSink, Cloneable {
+ Segment head;
+ long size;
+
+ public Buffer() {
+ }
+
+ /** Returns the number of bytes currently in this buffer. */
+ public long size() {
+ return size;
+ }
+
+ @Override public Buffer buffer() {
+ return this;
+ }
+
+ @Override public OutputStream outputStream() {
+ return new OutputStream() {
+ @Override public void write(int b) {
+ writeByte((byte) b);
+ }
+
+ @Override public void write(byte[] data, int offset, int byteCount) {
+ Buffer.this.write(data, offset, byteCount);
+ }
+
+ @Override public void flush() {
+ }
+
+ @Override public void close() {
+ }
+
+ @Override public String toString() {
+ return this + ".outputStream()";
+ }
+ };
+ }
+
+ @Override public Buffer emitCompleteSegments() {
+ return this; // Nowhere to emit to!
+ }
+
+ @Override public BufferedSink emit() throws IOException {
+ return this; // Nowhere to emit to!
+ }
+
+ @Override public boolean exhausted() {
+ return size == 0;
+ }
+
+ @Override public void require(long byteCount) throws EOFException {
+ if (size < byteCount) throw new EOFException();
+ }
+
+ @Override public boolean request(long byteCount) throws IOException {
+ return size >= byteCount;
+ }
+
+ @Override public InputStream inputStream() {
+ return new InputStream() {
+ @Override public int read() {
+ if (size > 0) return readByte() & 0xff;
+ return -1;
+ }
+
+ @Override public int read(byte[] sink, int offset, int byteCount) {
+ return Buffer.this.read(sink, offset, byteCount);
+ }
+
+ @Override public int available() {
+ return (int) Math.min(size, Integer.MAX_VALUE);
+ }
+
+ @Override public void close() {
+ }
+
+ @Override public String toString() {
+ return Buffer.this + ".inputStream()";
+ }
+ };
+ }
+
+ /** Copy the contents of this to {@code out}. */
+ public Buffer copyTo(OutputStream out) throws IOException {
+ return copyTo(out, 0, size);
+ }
+
+ /**
+ * Copy {@code byteCount} bytes from this, starting at {@code offset}, to
+ * {@code out}.
+ */
+ public Buffer copyTo(OutputStream out, long offset, long byteCount) throws IOException {
+ if (out == null) throw new IllegalArgumentException("out == null");
+ checkOffsetAndCount(size, offset, byteCount);
+ if (byteCount == 0) return this;
+
+ // Skip segments that we aren't copying from.
+ Segment s = head;
+ for (; offset >= (s.limit - s.pos); s = s.next) {
+ offset -= (s.limit - s.pos);
+ }
+
+ // Copy from one segment at a time.
+ for (; byteCount > 0; s = s.next) {
+ int pos = (int) (s.pos + offset);
+ int toCopy = (int) Math.min(s.limit - pos, byteCount);
+ out.write(s.data, pos, toCopy);
+ byteCount -= toCopy;
+ offset = 0;
+ }
+
+ return this;
+ }
+
+ /** Copy {@code byteCount} bytes from this, starting at {@code offset}, to {@code out}. */
+ public Buffer copyTo(Buffer out, long offset, long byteCount) {
+ if (out == null) throw new IllegalArgumentException("out == null");
+ checkOffsetAndCount(size, offset, byteCount);
+ if (byteCount == 0) return this;
+
+ Segment source = head;
+ Segment target = out.writableSegment(1);
+ out.size += byteCount;
+
+ while (byteCount > 0) {
+ // If necessary, advance to a readable source segment. This won't repeat after the first copy.
+ while (offset >= source.limit - source.pos) {
+ offset -= (source.limit - source.pos);
+ source = source.next;
+ }
+
+ // If necessary, append another target segment.
+ if (target.limit == Segment.SIZE) {
+ target = target.push(SegmentPool.INSTANCE.take());
+ }
+
+ // Copy bytes from the source segment to the target segment.
+ long sourceReadable = Math.min(source.limit - (source.pos + offset), byteCount);
+ long targetWritable = Segment.SIZE - target.limit;
+ int toCopy = (int) Math.min(sourceReadable, targetWritable);
+ System.arraycopy(source.data, source.pos + (int) offset, target.data, target.limit, toCopy);
+ offset += toCopy;
+ target.limit += toCopy;
+ byteCount -= toCopy;
+ }
+
+ return this;
+ }
+
+ /** Write the contents of this to {@code out}. */
+ public Buffer writeTo(OutputStream out) throws IOException {
+ return writeTo(out, size);
+ }
+
+ /** Write {@code byteCount} bytes from this to {@code out}. */
+ public Buffer writeTo(OutputStream out, long byteCount) throws IOException {
+ if (out == null) throw new IllegalArgumentException("out == null");
+ checkOffsetAndCount(size, 0, byteCount);
+
+ Segment s = head;
+ while (byteCount > 0) {
+ int toCopy = (int) Math.min(byteCount, s.limit - s.pos);
+ out.write(s.data, s.pos, toCopy);
+
+ s.pos += toCopy;
+ size -= toCopy;
+ byteCount -= toCopy;
+
+ if (s.pos == s.limit) {
+ Segment toRecycle = s;
+ head = s = toRecycle.pop();
+ SegmentPool.INSTANCE.recycle(toRecycle);
+ }
+ }
+
+ return this;
+ }
+
+ /** Read and exhaust bytes from {@code in} to this. */
+ public Buffer readFrom(InputStream in) throws IOException {
+ readFrom(in, Long.MAX_VALUE, true);
+ return this;
+ }
+
+ /** Read {@code byteCount} bytes from {@code in} to this. */
+ public Buffer readFrom(InputStream in, long byteCount) throws IOException {
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ readFrom(in, byteCount, false);
+ return this;
+ }
+
+ private void readFrom(InputStream in, long byteCount, boolean forever) throws IOException {
+ if (in == null) throw new IllegalArgumentException("in == null");
+ while (byteCount > 0 || forever) {
+ Segment tail = writableSegment(1);
+ int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
+ int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
+ if (bytesRead == -1) {
+ if (forever) return;
+ throw new EOFException();
+ }
+ tail.limit += bytesRead;
+ size += bytesRead;
+ byteCount -= bytesRead;
+ }
+ }
+
+ /**
+ * Returns the number of bytes in segments that are not writable. This is the
+ * number of bytes that can be flushed immediately to an underlying sink
+ * without harming throughput.
+ */
+ public long completeSegmentByteCount() {
+ long result = size;
+ if (result == 0) return 0;
+
+ // Omit the tail if it's still writable.
+ Segment tail = head.prev;
+ if (tail.limit < Segment.SIZE) {
+ result -= tail.limit - tail.pos;
+ }
+
+ return result;
+ }
+
+ @Override public byte readByte() {
+ if (size == 0) throw new IllegalStateException("size == 0");
+
+ Segment segment = head;
+ int pos = segment.pos;
+ int limit = segment.limit;
+
+ byte[] data = segment.data;
+ byte b = data[pos++];
+ size -= 1;
+
+ if (pos == limit) {
+ head = segment.pop();
+ SegmentPool.INSTANCE.recycle(segment);
+ } else {
+ segment.pos = pos;
+ }
+
+ return b;
+ }
+
+ /** Returns the byte at {@code pos}. */
+ public byte getByte(long pos) {
+ checkOffsetAndCount(size, pos, 1);
+ for (Segment s = head; true; s = s.next) {
+ int segmentByteCount = s.limit - s.pos;
+ if (pos < segmentByteCount) return s.data[s.pos + (int) pos];
+ pos -= segmentByteCount;
+ }
+ }
+
+ @Override public short readShort() {
+ if (size < 2) throw new IllegalStateException("size < 2: " + size);
+
+ Segment segment = head;
+ int pos = segment.pos;
+ int limit = segment.limit;
+
+ // If the short is split across multiple segments, delegate to readByte().
+ if (limit - pos < 2) {
+ int s = (readByte() & 0xff) << 8
+ | (readByte() & 0xff);
+ return (short) s;
+ }
+
+ byte[] data = segment.data;
+ int s = (data[pos++] & 0xff) << 8
+ | (data[pos++] & 0xff);
+ size -= 2;
+
+ if (pos == limit) {
+ head = segment.pop();
+ SegmentPool.INSTANCE.recycle(segment);
+ } else {
+ segment.pos = pos;
+ }
+
+ return (short) s;
+ }
+
+ @Override public int readInt() {
+ if (size < 4) throw new IllegalStateException("size < 4: " + size);
+
+ Segment segment = head;
+ int pos = segment.pos;
+ int limit = segment.limit;
+
+ // If the int is split across multiple segments, delegate to readByte().
+ if (limit - pos < 4) {
+ return (readByte() & 0xff) << 24
+ | (readByte() & 0xff) << 16
+ | (readByte() & 0xff) << 8
+ | (readByte() & 0xff);
+ }
+
+ byte[] data = segment.data;
+ int i = (data[pos++] & 0xff) << 24
+ | (data[pos++] & 0xff) << 16
+ | (data[pos++] & 0xff) << 8
+ | (data[pos++] & 0xff);
+ size -= 4;
+
+ if (pos == limit) {
+ head = segment.pop();
+ SegmentPool.INSTANCE.recycle(segment);
+ } else {
+ segment.pos = pos;
+ }
+
+ return i;
+ }
+
+ @Override public long readLong() {
+ if (size < 8) throw new IllegalStateException("size < 8: " + size);
+
+ Segment segment = head;
+ int pos = segment.pos;
+ int limit = segment.limit;
+
+ // If the long is split across multiple segments, delegate to readInt().
+ if (limit - pos < 8) {
+ return (readInt() & 0xffffffffL) << 32
+ | (readInt() & 0xffffffffL);
+ }
+
+ byte[] data = segment.data;
+ long v = (data[pos++] & 0xffL) << 56
+ | (data[pos++] & 0xffL) << 48
+ | (data[pos++] & 0xffL) << 40
+ | (data[pos++] & 0xffL) << 32
+ | (data[pos++] & 0xffL) << 24
+ | (data[pos++] & 0xffL) << 16
+ | (data[pos++] & 0xffL) << 8
+ | (data[pos++] & 0xffL);
+ size -= 8;
+
+ if (pos == limit) {
+ head = segment.pop();
+ SegmentPool.INSTANCE.recycle(segment);
+ } else {
+ segment.pos = pos;
+ }
+
+ return v;
+ }
+
+ @Override public short readShortLe() {
+ return Util.reverseBytesShort(readShort());
+ }
+
+ @Override public int readIntLe() {
+ return Util.reverseBytesInt(readInt());
+ }
+
+ @Override public long readLongLe() {
+ return Util.reverseBytesLong(readLong());
+ }
+
+ @Override public ByteString readByteString() {
+ return new ByteString(readByteArray());
+ }
+
+ @Override public ByteString readByteString(long byteCount) throws EOFException {
+ return new ByteString(readByteArray(byteCount));
+ }
+
+ @Override public void readFully(Buffer sink, long byteCount) throws EOFException {
+ if (size < byteCount) {
+ sink.write(this, size); // Exhaust ourselves.
+ throw new EOFException();
+ }
+ sink.write(this, byteCount);
+ }
+
+ @Override public long readAll(Sink sink) throws IOException {
+ long byteCount = size;
+ if (byteCount > 0) {
+ sink.write(this, byteCount);
+ }
+ return byteCount;
+ }
+
+ @Override public String readUtf8() {
+ try {
+ return readString(size, Util.UTF_8);
+ } catch (EOFException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override public String readUtf8(long byteCount) throws EOFException {
+ return readString(byteCount, Util.UTF_8);
+ }
+
+ @Override public String readString(Charset charset) {
+ try {
+ return readString(size, charset);
+ } catch (EOFException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override public String readString(long byteCount, Charset charset) throws EOFException {
+ checkOffsetAndCount(size, 0, byteCount);
+ if (charset == null) throw new IllegalArgumentException("charset == null");
+ if (byteCount > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
+ }
+ if (byteCount == 0) return "";
+
+ Segment s = head;
+ if (s.pos + byteCount > s.limit) {
+ // If the string spans multiple segments, delegate to readBytes().
+ return new String(readByteArray(byteCount), charset);
+ }
+
+ String result = new String(s.data, s.pos, (int) byteCount, charset);
+ s.pos += byteCount;
+ size -= byteCount;
+
+ if (s.pos == s.limit) {
+ head = s.pop();
+ SegmentPool.INSTANCE.recycle(s);
+ }
+
+ return result;
+ }
+
+ @Override public String readUtf8Line() throws EOFException {
+ long newline = indexOf((byte) '\n');
+
+ if (newline == -1) {
+ return size != 0 ? readUtf8(size) : null;
+ }
+
+ return readUtf8Line(newline);
+ }
+
+ @Override public String readUtf8LineStrict() throws EOFException {
+ long newline = indexOf((byte) '\n');
+ if (newline == -1) {
+ Buffer data = new Buffer();
+ copyTo(data, 0, Math.min(32, size));
+ throw new EOFException("\\n not found: size=" + size()
+ + " content=" + data.readByteString().hex() + "...");
+ }
+ return readUtf8Line(newline);
+ }
+
+ String readUtf8Line(long newline) throws EOFException {
+ if (newline > 0 && getByte(newline - 1) == '\r') {
+ // Read everything until '\r\n', then skip the '\r\n'.
+ String result = readUtf8((newline - 1));
+ skip(2);
+ return result;
+
+ } else {
+ // Read everything until '\n', then skip the '\n'.
+ String result = readUtf8(newline);
+ skip(1);
+ return result;
+ }
+ }
+
+ @Override public byte[] readByteArray() {
+ try {
+ return readByteArray(size);
+ } catch (EOFException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ @Override public byte[] readByteArray(long byteCount) throws EOFException {
+ checkOffsetAndCount(size, 0, byteCount);
+ if (byteCount > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
+ }
+
+ byte[] result = new byte[(int) byteCount];
+ readFully(result);
+ return result;
+ }
+
+ @Override public int read(byte[] sink) {
+ return read(sink, 0, sink.length);
+ }
+
+ @Override public void readFully(byte[] sink) throws EOFException {
+ int offset = 0;
+ while (offset < sink.length) {
+ int read = read(sink, offset, sink.length - offset);
+ if (read == -1) throw new EOFException();
+ offset += read;
+ }
+ }
+
+ @Override public int read(byte[] sink, int offset, int byteCount) {
+ checkOffsetAndCount(sink.length, offset, byteCount);
+
+ Segment s = head;
+ if (s == null) return -1;
+ int toCopy = Math.min(byteCount, s.limit - s.pos);
+ System.arraycopy(s.data, s.pos, sink, offset, toCopy);
+
+ s.pos += toCopy;
+ size -= toCopy;
+
+ if (s.pos == s.limit) {
+ head = s.pop();
+ SegmentPool.INSTANCE.recycle(s);
+ }
+
+ return toCopy;
+ }
+
+ /**
+ * Discards all bytes in this buffer. Calling this method when you're done
+ * with a buffer will return its segments to the pool.
+ */
+ public void clear() {
+ try {
+ skip(size);
+ } catch (EOFException e) {
+ throw new AssertionError(e);
+ }
+ }
+
+ /** Discards {@code byteCount} bytes from the head of this buffer. */
+ @Override public void skip(long byteCount) throws EOFException {
+ while (byteCount > 0) {
+ if (head == null) throw new EOFException();
+
+ int toSkip = (int) Math.min(byteCount, head.limit - head.pos);
+ size -= toSkip;
+ byteCount -= toSkip;
+ head.pos += toSkip;
+
+ if (head.pos == head.limit) {
+ Segment toRecycle = head;
+ head = toRecycle.pop();
+ SegmentPool.INSTANCE.recycle(toRecycle);
+ }
+ }
+ }
+
+ @Override public Buffer write(ByteString byteString) {
+ if (byteString == null) throw new IllegalArgumentException("byteString == null");
+ return write(byteString.data, 0, byteString.data.length);
+ }
+
+ @Override public Buffer writeUtf8(String string) {
+ if (string == null) throw new IllegalArgumentException("string == null");
+
+ // Transcode a UTF-16 Java String to UTF-8 bytes.
+ for (int i = 0, length = string.length(); i < length;) {
+ int c = string.charAt(i);
+
+ if (c < 0x80) {
+ Segment tail = writableSegment(1);
+ byte[] data = tail.data;
+ int segmentOffset = tail.limit - i;
+ int runLimit = Math.min(length, Segment.SIZE - segmentOffset);
+
+ // Emit a 7-bit character with 1 byte.
+ data[segmentOffset + i++] = (byte) c; // 0xxxxxxx
+
+ // Fast-path contiguous runs of ASCII characters. This is ugly, but yields a ~4x performance
+ // improvement over independent calls to writeByte().
+ while (i < runLimit) {
+ c = string.charAt(i);
+ if (c >= 0x80) break;
+ data[segmentOffset + i++] = (byte) c; // 0xxxxxxx
+ }
+
+ int runSize = i + segmentOffset - tail.limit; // Equivalent to i - (previous i).
+ tail.limit += runSize;
+ size += runSize;
+
+ } else if (c < 0x800) {
+ // Emit a 11-bit character with 2 bytes.
+ writeByte(c >> 6 | 0xc0); // 110xxxxx
+ writeByte(c & 0x3f | 0x80); // 10xxxxxx
+ i++;
+
+ } else if (c < 0xd800 || c > 0xdfff) {
+ // Emit a 16-bit character with 3 bytes.
+ writeByte(c >> 12 | 0xe0); // 1110xxxx
+ writeByte(c >> 6 & 0x3f | 0x80); // 10xxxxxx
+ writeByte(c & 0x3f | 0x80); // 10xxxxxx
+ i++;
+
+ } else {
+ // c is a surrogate. Make sure it is a high surrogate & that its successor is a low
+ // surrogate. If not, the UTF-16 is invalid, in which case we emit a replacement character.
+ int low = i + 1 < length ? string.charAt(i + 1) : 0;
+ if (c > 0xdbff || low < 0xdc00 || low > 0xdfff) {
+ writeByte('?');
+ i++;
+ continue;
+ }
+
+ // UTF-16 high surrogate: 110110xxxxxxxxxx (10 bits)
+ // UTF-16 low surrogate: 110111yyyyyyyyyy (10 bits)
+ // Unicode code point: 00010000000000000000 + xxxxxxxxxxyyyyyyyyyy (21 bits)
+ int codePoint = 0x010000 + ((c & ~0xd800) << 10 | low & ~0xdc00);
+
+ // Emit a 21-bit character with 4 bytes.
+ writeByte(codePoint >> 18 | 0xf0); // 11110xxx
+ writeByte(codePoint >> 12 & 0x3f | 0x80); // 10xxxxxx
+ writeByte(codePoint >> 6 & 0x3f | 0x80); // 10xxyyyy
+ writeByte(codePoint & 0x3f | 0x80); // 10yyyyyy
+ i += 2;
+ }
+ }
+
+ return this;
+ }
+
+ @Override public Buffer writeString(String string, Charset charset) {
+ if (string == null) throw new IllegalArgumentException("string == null");
+ if (charset == null) throw new IllegalArgumentException("charset == null");
+ if (charset.equals(Util.UTF_8)) return writeUtf8(string);
+ byte[] data = string.getBytes(charset);
+ return write(data, 0, data.length);
+ }
+
+ @Override public Buffer write(byte[] source) {
+ if (source == null) throw new IllegalArgumentException("source == null");
+ return write(source, 0, source.length);
+ }
+
+ @Override public Buffer write(byte[] source, int offset, int byteCount) {
+ if (source == null) throw new IllegalArgumentException("source == null");
+ checkOffsetAndCount(source.length, offset, byteCount);
+
+ int limit = offset + byteCount;
+ while (offset < limit) {
+ Segment tail = writableSegment(1);
+
+ int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
+ System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
+
+ offset += toCopy;
+ tail.limit += toCopy;
+ }
+
+ size += byteCount;
+ return this;
+ }
+
+ @Override public long writeAll(Source source) throws IOException {
+ if (source == null) throw new IllegalArgumentException("source == null");
+ long totalBytesRead = 0;
+ for (long readCount; (readCount = source.read(this, Segment.SIZE)) != -1; ) {
+ totalBytesRead += readCount;
+ }
+ return totalBytesRead;
+ }
+
+ @Override public BufferedSink write(Source source, long byteCount) throws IOException {
+ if (byteCount > 0) {
+ source.read(this, byteCount);
+ }
+ return this;
+ }
+
+ @Override public Buffer writeByte(int b) {
+ Segment tail = writableSegment(1);
+ tail.data[tail.limit++] = (byte) b;
+ size += 1;
+ return this;
+ }
+
+ @Override public Buffer writeShort(int s) {
+ Segment tail = writableSegment(2);
+ byte[] data = tail.data;
+ int limit = tail.limit;
+ data[limit++] = (byte) ((s >>> 8) & 0xff);
+ data[limit++] = (byte) (s & 0xff);
+ tail.limit = limit;
+ size += 2;
+ return this;
+ }
+
+ @Override public Buffer writeShortLe(int s) {
+ return writeShort(Util.reverseBytesShort((short) s));
+ }
+
+ @Override public Buffer writeInt(int i) {
+ Segment tail = writableSegment(4);
+ byte[] data = tail.data;
+ int limit = tail.limit;
+ data[limit++] = (byte) ((i >>> 24) & 0xff);
+ data[limit++] = (byte) ((i >>> 16) & 0xff);
+ data[limit++] = (byte) ((i >>> 8) & 0xff);
+ data[limit++] = (byte) (i & 0xff);
+ tail.limit = limit;
+ size += 4;
+ return this;
+ }
+
+ @Override public Buffer writeIntLe(int i) {
+ return writeInt(Util.reverseBytesInt(i));
+ }
+
+ @Override public Buffer writeLong(long v) {
+ Segment tail = writableSegment(8);
+ byte[] data = tail.data;
+ int limit = tail.limit;
+ data[limit++] = (byte) ((v >>> 56L) & 0xff);
+ data[limit++] = (byte) ((v >>> 48L) & 0xff);
+ data[limit++] = (byte) ((v >>> 40L) & 0xff);
+ data[limit++] = (byte) ((v >>> 32L) & 0xff);
+ data[limit++] = (byte) ((v >>> 24L) & 0xff);
+ data[limit++] = (byte) ((v >>> 16L) & 0xff);
+ data[limit++] = (byte) ((v >>> 8L) & 0xff);
+ data[limit++] = (byte) (v & 0xff);
+ tail.limit = limit;
+ size += 8;
+ return this;
+ }
+
+ @Override public Buffer writeLongLe(long v) {
+ return writeLong(reverseBytesLong(v));
+ }
+
+ /**
+ * Returns a tail segment that we can write at least {@code minimumCapacity}
+ * bytes to, creating it if necessary.
+ */
+ Segment writableSegment(int minimumCapacity) {
+ if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
+
+ if (head == null) {
+ head = SegmentPool.INSTANCE.take(); // Acquire a first segment.
+ return head.next = head.prev = head;
+ }
+
+ Segment tail = head.prev;
+ if (tail.limit + minimumCapacity > Segment.SIZE) {
+ tail = tail.push(SegmentPool.INSTANCE.take()); // Append a new empty segment to fill up.
+ }
+ return tail;
+ }
+
+ @Override public void write(Buffer source, long byteCount) {
+ // Move bytes from the head of the source buffer to the tail of this buffer
+ // while balancing two conflicting goals: don't waste CPU and don't waste
+ // memory.
+ //
+ //
+ // Don't waste CPU (ie. don't copy data around).
+ //
+ // Copying large amounts of data is expensive. Instead, we prefer to
+ // reassign entire segments from one buffer to the other.
+ //
+ //
+ // Don't waste memory.
+ //
+ // As an invariant, adjacent pairs of segments in a buffer should be at
+ // least 50% full, except for the head segment and the tail segment.
+ //
+ // The head segment cannot maintain the invariant because the application is
+ // consuming bytes from this segment, decreasing its level.
+ //
+ // The tail segment cannot maintain the invariant because the application is
+ // producing bytes, which may require new nearly-empty tail segments to be
+ // appended.
+ //
+ //
+ // Moving segments between buffers
+ //
+ // When writing one buffer to another, we prefer to reassign entire segments
+ // over copying bytes into their most compact form. Suppose we have a buffer
+ // with these segment levels [91%, 61%]. If we append a buffer with a
+ // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied.
+ //
+ // Or suppose we have a buffer with these segment levels: [100%, 2%], and we
+ // want to append it to a buffer with these segment levels [99%, 3%]. This
+ // operation will yield the following segments: [100%, 2%, 99%, 3%]. That
+ // is, we do not spend time copying bytes around to achieve more efficient
+ // memory use like [100%, 100%, 4%].
+ //
+ // When combining buffers, we will compact adjacent buffers when their
+ // combined level doesn't exceed 100%. For example, when we start with
+ // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%].
+ //
+ //
+ // Splitting segments
+ //
+ // Occasionally we write only part of a source buffer to a sink buffer. For
+ // example, given a sink [51%, 91%], we may want to write the first 30% of
+ // a source [92%, 82%] to it. To simplify, we first transform the source to
+ // an equivalent buffer [30%, 62%, 82%] and then move the head segment,
+ // yielding sink [51%, 91%, 30%] and source [62%, 82%].
+
+ if (source == null) throw new IllegalArgumentException("source == null");
+ if (source == this) throw new IllegalArgumentException("source == this");
+ checkOffsetAndCount(source.size, 0, byteCount);
+
+ while (byteCount > 0) {
+ // Is a prefix of the source's head segment all that we need to move?
+ if (byteCount < (source.head.limit - source.head.pos)) {
+ Segment tail = head != null ? head.prev : null;
+ if (tail == null || byteCount + (tail.limit - tail.pos) > Segment.SIZE) {
+ // We're going to need another segment. Split the source's head
+ // segment in two, then move the first of those two to this buffer.
+ source.head = source.head.split((int) byteCount);
+ } else {
+ // Our existing segments are sufficient. Move bytes from source's head to our tail.
+ source.head.writeTo(tail, (int) byteCount);
+ source.size -= byteCount;
+ size += byteCount;
+ return;
+ }
+ }
+
+ // Remove the source's head segment and append it to our tail.
+ Segment segmentToMove = source.head;
+ long movedByteCount = segmentToMove.limit - segmentToMove.pos;
+ source.head = segmentToMove.pop();
+ if (head == null) {
+ head = segmentToMove;
+ head.next = head.prev = head;
+ } else {
+ Segment tail = head.prev;
+ tail = tail.push(segmentToMove);
+ tail.compact();
+ }
+ source.size -= movedByteCount;
+ size += movedByteCount;
+ byteCount -= movedByteCount;
+ }
+ }
+
+ @Override public long read(Buffer sink, long byteCount) {
+ if (sink == null) throw new IllegalArgumentException("sink == null");
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ if (size == 0) return -1L;
+ if (byteCount > size) byteCount = size;
+ sink.write(this, byteCount);
+ return byteCount;
+ }
+
+ @Override public long indexOf(byte b) {
+ return indexOf(b, 0);
+ }
+
+ /**
+ * Returns the index of {@code b} in this at or beyond {@code fromIndex}, or
+ * -1 if this buffer does not contain {@code b} in that range.
+ */
+ @Override public long indexOf(byte b, long fromIndex) {
+ if (fromIndex < 0) throw new IllegalArgumentException("fromIndex < 0");
+
+ Segment s = head;
+ if (s == null) return -1L;
+ long offset = 0L;
+ do {
+ int segmentByteCount = s.limit - s.pos;
+ if (fromIndex >= segmentByteCount) {
+ fromIndex -= segmentByteCount;
+ } else {
+ byte[] data = s.data;
+ for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) {
+ if (data[(int) pos] == b) return offset + pos - s.pos;
+ }
+ fromIndex = 0;
+ }
+ offset += segmentByteCount;
+ s = s.next;
+ } while (s != head);
+ return -1L;
+ }
+
+ @Override public long indexOfElement(ByteString targetBytes) {
+ return indexOfElement(targetBytes, 0);
+ }
+
+ @Override public long indexOfElement(ByteString targetBytes, long fromIndex) {
+ if (fromIndex < 0) throw new IllegalArgumentException("fromIndex < 0");
+
+ Segment s = head;
+ if (s == null) return -1L;
+ long offset = 0L;
+ byte[] toFind = targetBytes.data;
+ do {
+ int segmentByteCount = s.limit - s.pos;
+ if (fromIndex >= segmentByteCount) {
+ fromIndex -= segmentByteCount;
+ } else {
+ byte[] data = s.data;
+ for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) {
+ byte b = data[(int) pos];
+ for (byte targetByte : toFind) {
+ if (b == targetByte) return offset + pos - s.pos;
+ }
+ }
+ fromIndex = 0;
+ }
+ offset += segmentByteCount;
+ s = s.next;
+ } while (s != head);
+ return -1L;
+ }
+
+ @Override public void flush() {
+ }
+
+ @Override public void close() {
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ /** For testing. This returns the sizes of the segments in this buffer. */
+ List<Integer> segmentSizes() {
+ if (head == null) return Collections.emptyList();
+ List<Integer> result = new ArrayList<>();
+ result.add(head.limit - head.pos);
+ for (Segment s = head.next; s != head; s = s.next) {
+ result.add(s.limit - s.pos);
+ }
+ return result;
+ }
+
+ @Override public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Buffer)) return false;
+ Buffer that = (Buffer) o;
+ if (size != that.size) return false;
+ if (size == 0) return true; // Both buffers are empty.
+
+ Segment sa = this.head;
+ Segment sb = that.head;
+ int posA = sa.pos;
+ int posB = sb.pos;
+
+ for (long pos = 0, count; pos < size; pos += count) {
+ count = Math.min(sa.limit - posA, sb.limit - posB);
+
+ for (int i = 0; i < count; i++) {
+ if (sa.data[posA++] != sb.data[posB++]) return false;
+ }
+
+ if (posA == sa.limit) {
+ sa = sa.next;
+ posA = sa.pos;
+ }
+
+ if (posB == sb.limit) {
+ sb = sb.next;
+ posB = sb.pos;
+ }
+ }
+
+ return true;
+ }
+
+ @Override public int hashCode() {
+ Segment s = head;
+ if (s == null) return 0;
+ int result = 1;
+ do {
+ for (int pos = s.pos, limit = s.limit; pos < limit; pos++) {
+ result = 31 * result + s.data[pos];
+ }
+ s = s.next;
+ } while (s != head);
+ return result;
+ }
+
+ @Override public String toString() {
+ if (size == 0) {
+ return "Buffer[size=0]";
+ }
+
+ if (size <= 16) {
+ ByteString data = clone().readByteString();
+ return String.format("Buffer[size=%s data=%s]", size, data.hex());
+ }
+
+ try {
+ MessageDigest md5 = MessageDigest.getInstance("MD5");
+ md5.update(head.data, head.pos, head.limit - head.pos);
+ for (Segment s = head.next; s != head; s = s.next) {
+ md5.update(s.data, s.pos, s.limit - s.pos);
+ }
+ return String.format("Buffer[size=%s md5=%s]",
+ size, ByteString.of(md5.digest()).hex());
+ } catch (NoSuchAlgorithmException e) {
+ throw new AssertionError();
+ }
+ }
+
+ /** Returns a deep copy of this buffer. */
+ @Override public Buffer clone() {
+ Buffer result = new Buffer();
+ if (size == 0) return result;
+
+ result.write(head.data, head.pos, head.limit - head.pos);
+ for (Segment s = head.next; s != head; s = s.next) {
+ result.write(s.data, s.pos, s.limit - s.pos);
+ }
+
+ return result;
+ }
+}
diff --git a/okio/src/main/java/okio/BufferedSink.java b/okio/okio/src/main/java/okio/BufferedSink.java
similarity index 63%
rename from okio/src/main/java/okio/BufferedSink.java
rename to okio/okio/src/main/java/okio/BufferedSink.java
index 3066011..0deba92 100644
--- a/okio/src/main/java/okio/BufferedSink.java
+++ b/okio/okio/src/main/java/okio/BufferedSink.java
@@ -17,6 +17,7 @@
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.charset.Charset;
/**
* A sink that keeps a buffer internally so that callers can do small writes
@@ -24,25 +25,37 @@
*/
public interface BufferedSink extends Sink {
/** Returns this sink's internal buffer. */
- OkBuffer buffer();
+ Buffer buffer();
BufferedSink write(ByteString byteString) throws IOException;
/**
- * Like {@link OutputStream#write}, this writes a complete byte array to this
- * sink.
+ * Like {@link OutputStream#write(byte[])}, this writes a complete byte array to
+ * this sink.
*/
BufferedSink write(byte[] source) throws IOException;
/**
- * Like {@link OutputStream#write}, this writes {@code byteCount} bytes
- * of {@code source}, starting at {@code offset}.
+ * Like {@link OutputStream#write(byte[], int, int)}, this writes {@code byteCount}
+ * bytes of {@code source}, starting at {@code offset}.
*/
BufferedSink write(byte[] source, int offset, int byteCount) throws IOException;
+ /**
+ * Removes all bytes from {@code source} and appends them to this sink. Returns the
+ * number of bytes read which will be 0 if {@code source} is exhausted.
+ */
+ long writeAll(Source source) throws IOException;
+
+ /** Removes {@code byteCount} bytes from {@code source} and appends them to this sink. */
+ BufferedSink write(Source source, long byteCount) throws IOException;
+
/** Encodes {@code string} in UTF-8 and writes it to this sink. */
BufferedSink writeUtf8(String string) throws IOException;
+ /** Encodes {@code string} in {@code charset} and writes it to this sink. */
+ BufferedSink writeString(String string, Charset charset) throws IOException;
+
/** Writes a byte to this sink. */
BufferedSink writeByte(int b) throws IOException;
@@ -64,9 +77,19 @@
/** Writes a little-endian long to this sink using eight bytes. */
BufferedSink writeLongLe(long v) throws IOException;
- /** Writes complete segments to this sink. Like {@link #flush}, but weaker. */
+ /**
+ * Writes complete segments to the underlying sink, if one exists. Like {@link #flush}, but
+ * weaker. Use this to limit the memory held in the buffer to a single segment.
+ */
BufferedSink emitCompleteSegments() throws IOException;
+ /**
+ * Writes all buffered data to the underlying sink, if one exists. Like {@link #flush}, but
+ * weaker. Call this before this buffered sink goes out of scope so that its data can reach its
+ * destination.
+ */
+ BufferedSink emit() throws IOException;
+
/** Returns an output stream that writes to this sink. */
OutputStream outputStream();
}
diff --git a/okio/okio/src/main/java/okio/BufferedSource.java b/okio/okio/src/main/java/okio/BufferedSource.java
new file mode 100644
index 0000000..b13f1bc
--- /dev/null
+++ b/okio/okio/src/main/java/okio/BufferedSource.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+/**
+ * A source that keeps a buffer internally so that callers can do small reads
+ * without a performance penalty. It also allows clients to read ahead,
+ * buffering as much as necessary before consuming input.
+ */
+public interface BufferedSource extends Source {
+ /** Returns this source's internal buffer. */
+ Buffer buffer();
+
+ /**
+ * Returns true if there are no more bytes in this source. This will block
+ * until there are bytes to read or the source is definitely exhausted.
+ */
+ boolean exhausted() throws IOException;
+
+ /**
+ * Returns when the buffer contains at least {@code byteCount} bytes. Throws
+ * an {@link java.io.EOFException} if the source is exhausted before the
+ * required bytes can be read.
+ */
+ void require(long byteCount) throws IOException;
+
+ /**
+ * Returns true when the buffer contains at least {@code byteCount} bytes,
+ * expanding it as necessary. Returns false if the source is exhausted before
+ * the requested bytes can be read.
+ */
+ boolean request(long byteCount) throws IOException;
+
+ /** Removes a byte from this source and returns it. */
+ byte readByte() throws IOException;
+
+ /** Removes two bytes from this source and returns a big-endian short. */
+ short readShort() throws IOException;
+
+ /** Removes two bytes from this source and returns a little-endian short. */
+ short readShortLe() throws IOException;
+
+ /** Removes four bytes from this source and returns a big-endian int. */
+ int readInt() throws IOException;
+
+ /** Removes four bytes from this source and returns a little-endian int. */
+ int readIntLe() throws IOException;
+
+ /** Removes eight bytes from this source and returns a big-endian long. */
+ long readLong() throws IOException;
+
+ /** Removes eight bytes from this source and returns a little-endian long. */
+ long readLongLe() throws IOException;
+
+ /**
+ * Reads and discards {@code byteCount} bytes from this source. Throws an
+ * {@link java.io.EOFException} if the source is exhausted before the
+ * requested bytes can be skipped.
+ */
+ void skip(long byteCount) throws IOException;
+
+ /** Removes all bytes bytes from this and returns them as a byte string. */
+ ByteString readByteString() throws IOException;
+
+ /** Removes {@code byteCount} bytes from this and returns them as a byte string. */
+ ByteString readByteString(long byteCount) throws IOException;
+
+ /** Removes all bytes from this and returns them as a byte array. */
+ byte[] readByteArray() throws IOException;
+
+ /** Removes {@code byteCount} bytes from this and returns them as a byte array. */
+ byte[] readByteArray(long byteCount) throws IOException;
+
+ /**
+ * Removes up to {@code sink.length} bytes from this and copies them into {@code sink}.
+ * Returns the number of bytes read, or -1 if this source is exhausted.
+ */
+ int read(byte[] sink) throws IOException;
+
+ /**
+ * Removes exactly {@code sink.length} bytes from this and copies them into {@code sink}.
+ * Throws an {@link java.io.EOFException} if the requested number of bytes cannot be read.
+ */
+ void readFully(byte[] sink) throws IOException;
+
+ /**
+ * Removes up to {@code byteCount} bytes from this and copies them into {@code sink} at
+ * {@code offset}. Returns the number of bytes read, or -1 if this source is exhausted.
+ */
+ int read(byte[] sink, int offset, int byteCount) throws IOException;
+
+ /**
+ * Removes exactly {@code byteCount} bytes from this and appends them to
+ * {@code sink}. Throws an {@link java.io.EOFException} if the requested
+ * number of bytes cannot be read.
+ */
+ void readFully(Buffer sink, long byteCount) throws IOException;
+
+ /**
+ * Removes all bytes from this and appends them to {@code sink}. Returns the
+ * total number of bytes written to {@code sink} which will be 0 if this is
+ * exhausted.
+ */
+ long readAll(Sink sink) throws IOException;
+
+ /** Removes all bytes from this, decodes them as UTF-8, and returns the string. */
+ String readUtf8() throws IOException;
+
+ /**
+ * Removes {@code byteCount} bytes from this, decodes them as UTF-8, and
+ * returns the string.
+ */
+ String readUtf8(long byteCount) throws IOException;
+
+ /**
+ * Removes and returns characters up to but not including the next line break.
+ * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
+ * not included in the result.
+ *
+ * <p><strong>On the end of the stream this method returns null,</strong> just
+ * like {@link java.io.BufferedReader}. If the source doesn't end with a line
+ * break then an implicit line break is assumed. Null is returned once the
+ * source is exhausted. Use this for human-generated data, where a trailing
+ * line break is optional.
+ */
+ String readUtf8Line() throws IOException;
+
+ /**
+ * Removes and returns characters up to but not including the next line break.
+ * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
+ * not included in the result.
+ *
+ * <p><strong>On the end of the stream this method throws.</strong> Every call
+ * must consume either '\r\n' or '\n'. If these characters are absent in the
+ * stream, an {@link java.io.EOFException} is thrown. Use this for
+ * machine-generated data where a missing line break implies truncated input.
+ */
+ String readUtf8LineStrict() throws IOException;
+
+ /**
+ * Removes all bytes from this, decodes them as {@code charset}, and returns
+ * the string.
+ */
+ String readString(Charset charset) throws IOException;
+
+ /**
+ * Removes {@code byteCount} bytes from this, decodes them as {@code charset},
+ * and returns the string.
+ */
+ String readString(long byteCount, Charset charset) throws IOException;
+
+ /**
+ * Returns the index of the first {@code b} in the buffer. This expands the
+ * buffer as necessary until {@code b} is found. This reads an unbounded
+ * number of bytes into the buffer. Returns -1 if the stream is exhausted
+ * before the requested byte is found.
+ */
+ long indexOf(byte b) throws IOException;
+
+ /**
+ * Returns the index of the first {@code b} in the buffer at or after {@code
+ * fromIndex}. This expands the buffer as necessary until {@code b} is found.
+ * This reads an unbounded number of bytes into the buffer. Returns -1 if the
+ * stream is exhausted before the requested byte is found.
+ */
+ long indexOf(byte b, long fromIndex) throws IOException;
+
+ /**
+ * Returns the index of the first byte in {@code targetBytes} in the buffer.
+ * This expands the buffer as necessary until a target byte is found. This
+ * reads an unbounded number of bytes into the buffer. Returns -1 if the
+ * stream is exhausted before the requested byte is found.
+ */
+ long indexOfElement(ByteString targetBytes) throws IOException;
+
+ /**
+ * Returns the index of the first byte in {@code targetBytes} in the buffer
+ * at or after {@code fromIndex}. This expands the buffer as necessary until
+ * a target byte is found. This reads an unbounded number of bytes into the
+ * buffer. Returns -1 if the stream is exhausted before the requested byte is
+ * found.
+ */
+ long indexOfElement(ByteString targetBytes, long fromIndex) throws IOException;
+
+ /** Returns an input stream that reads from this source. */
+ InputStream inputStream();
+}
diff --git a/okio/src/main/java/okio/ByteString.java b/okio/okio/src/main/java/okio/ByteString.java
similarity index 71%
rename from okio/src/main/java/okio/ByteString.java
rename to okio/okio/src/main/java/okio/ByteString.java
index 6853adb..a42fbe6 100644
--- a/okio/src/main/java/okio/ByteString.java
+++ b/okio/okio/src/main/java/okio/ByteString.java
@@ -18,11 +18,17 @@
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import java.io.OutputStream;
+import java.io.Serializable;
+import java.lang.reflect.Field;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import static okio.Util.checkOffsetAndCount;
+
/**
* An immutable sequence of bytes.
*
@@ -34,9 +40,10 @@
* and other environments that run both trusted and untrusted code in the same
* process.
*/
-public final class ByteString {
+public final class ByteString implements Serializable {
private static final char[] HEX_DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ private static final long serialVersionUID = 1L;
/** A singleton empty {@code ByteString}. */
public static final ByteString EMPTY = ByteString.of();
@@ -53,11 +60,26 @@
* Returns a new byte string containing a clone of the bytes of {@code data}.
*/
public static ByteString of(byte... data) {
+ if (data == null) throw new IllegalArgumentException("data == null");
return new ByteString(data.clone());
}
+ /**
+ * Returns a new byte string containing a copy of {@code byteCount} bytes of {@code data} starting
+ * at {@code offset}.
+ */
+ public static ByteString of(byte[] data, int offset, int byteCount) {
+ if (data == null) throw new IllegalArgumentException("data == null");
+ checkOffsetAndCount(data.length, offset, byteCount);
+
+ byte[] copy = new byte[byteCount];
+ System.arraycopy(data, offset, copy, 0, byteCount);
+ return new ByteString(copy);
+ }
+
/** Returns a new byte string containing the {@code UTF-8} bytes of {@code s}. */
public static ByteString encodeUtf8(String s) {
+ if (s == null) throw new IllegalArgumentException("s == null");
ByteString byteString = new ByteString(s.getBytes(Util.UTF_8));
byteString.utf8 = s;
return byteString;
@@ -84,6 +106,7 @@
* Returns null if {@code base64} is not a Base64-encoded sequence of bytes.
*/
public static ByteString decodeBase64(String base64) {
+ if (base64 == null) throw new IllegalArgumentException("base64 == null");
byte[] decoded = Base64.decode(base64);
return decoded != null ? new ByteString(decoded) : null;
}
@@ -101,6 +124,7 @@
/** Decodes the hex-encoded bytes and returns their value a byte string. */
public static ByteString decodeHex(String hex) {
+ if (hex == null) throw new IllegalArgumentException("hex == null");
if (hex.length() % 2 != 0) throw new IllegalArgumentException("Unexpected hex string: " + hex);
byte[] result = new byte[hex.length() / 2];
@@ -126,6 +150,9 @@
* bytes to read.
*/
public static ByteString read(InputStream in, int byteCount) throws IOException {
+ if (in == null) throw new IllegalArgumentException("in == null");
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+
byte[] result = new byte[byteCount];
for (int offset = 0, read; offset < byteCount; offset += read) {
read = in.read(result, offset, byteCount - offset);
@@ -159,6 +186,31 @@
return this;
}
+ /**
+ * Returns a byte string equal to this byte string, but with the bytes 'a'
+ * through 'z' replaced with the corresponding byte in 'A' through 'Z'.
+ * Returns this byte string if it contains no bytes in 'a' through 'z'.
+ */
+ public ByteString toAsciiUppercase() {
+ // Search for an lowercase character. If we don't find one, return this.
+ for (int i = 0; i < data.length; i++) {
+ byte c = data[i];
+ if (c < 'a' || c > 'z') continue;
+
+ // If we reach this point, this string is not not uppercase. Create and
+ // return a new byte string.
+ byte[] lowercase = data.clone();
+ lowercase[i++] = (byte) (c - ('a' - 'A'));
+ for (; i < lowercase.length; i++) {
+ c = lowercase[i];
+ if (c < 'a' || c > 'z') continue;
+ lowercase[i] = (byte) (c - ('a' - 'A'));
+ }
+ return new ByteString(lowercase);
+ }
+ return this;
+ }
+
/** Returns the byte at {@code pos}. */
public byte getByte(int pos) {
return data[pos];
@@ -180,6 +232,7 @@
/** Writes the contents of this byte string to {@code out}. */
public void write(OutputStream out) throws IOException {
+ if (out == null) throw new IllegalArgumentException("out == null");
out.write(data);
}
@@ -208,4 +261,23 @@
throw new AssertionError();
}
}
+
+ private void readObject(ObjectInputStream in) throws IOException {
+ int dataLength = in.readInt();
+ ByteString byteString = ByteString.read(in, dataLength);
+ try {
+ Field field = ByteString.class.getDeclaredField("data");
+ field.setAccessible(true);
+ field.set(this, byteString.data);
+ } catch (NoSuchFieldException e) {
+ throw new AssertionError();
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+
+ private void writeObject(ObjectOutputStream out) throws IOException {
+ out.writeInt(data.length);
+ out.write(data);
+ }
}
diff --git a/okio/src/main/java/okio/DeflaterSink.java b/okio/okio/src/main/java/okio/DeflaterSink.java
similarity index 82%
rename from okio/src/main/java/okio/DeflaterSink.java
rename to okio/okio/src/main/java/okio/DeflaterSink.java
index 7249f2d..c8b5f67 100644
--- a/okio/src/main/java/okio/DeflaterSink.java
+++ b/okio/okio/src/main/java/okio/DeflaterSink.java
@@ -40,11 +40,22 @@
private boolean closed;
public DeflaterSink(Sink sink, Deflater deflater) {
- this.sink = Okio.buffer(sink);
+ this(Okio.buffer(sink), deflater);
+ }
+
+ /**
+ * This package-private constructor shares a buffer with its trusted caller.
+ * In general we can't share a BufferedSource because the deflater holds input
+ * bytes until they are inflated.
+ */
+ DeflaterSink(BufferedSink sink, Deflater deflater) {
+ if (sink == null) throw new IllegalArgumentException("source == null");
+ if (deflater == null) throw new IllegalArgumentException("inflater == null");
+ this.sink = sink;
this.deflater = deflater;
}
- @Override public void write(OkBuffer source, long byteCount)
+ @Override public void write(Buffer source, long byteCount)
throws IOException {
checkOffsetAndCount(source.size, 0, byteCount);
while (byteCount > 0) {
@@ -68,8 +79,11 @@
}
}
+ // ANDROID-BEGIN
+ // @IgnoreJRERequirement
+ // ANDROID-END
private void deflate(boolean syncFlush) throws IOException {
- OkBuffer buffer = sink.buffer();
+ Buffer buffer = sink.buffer();
while (true) {
Segment s = buffer.writableSegment(1);
@@ -96,6 +110,11 @@
sink.flush();
}
+ void finishDeflate() throws IOException {
+ deflater.finish();
+ deflate(false);
+ }
+
@Override public void close() throws IOException {
if (closed) return;
@@ -103,8 +122,7 @@
// to close the deflater and the sink; otherwise we risk leaking resources.
Throwable thrown = null;
try {
- deflater.finish();
- deflate(false);
+ finishDeflate();
} catch (Throwable e) {
thrown = e;
}
@@ -125,9 +143,8 @@
if (thrown != null) Util.sneakyRethrow(thrown);
}
- @Override public Sink deadline(Deadline deadline) {
- sink.deadline(deadline);
- return this;
+ @Override public Timeout timeout() {
+ return sink.timeout();
}
@Override public String toString() {
diff --git a/okio/okio/src/main/java/okio/ForwardingSink.java b/okio/okio/src/main/java/okio/ForwardingSink.java
new file mode 100644
index 0000000..6bf7a14
--- /dev/null
+++ b/okio/okio/src/main/java/okio/ForwardingSink.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+
+/** A {@link Sink} which forwards calls to another. Useful for subclassing. */
+public abstract class ForwardingSink implements Sink {
+ private final Sink delegate;
+
+ public ForwardingSink(Sink delegate) {
+ if (delegate == null) throw new IllegalArgumentException("delegate == null");
+ this.delegate = delegate;
+ }
+
+ /** {@link Sink} to which this instance is delegating. */
+ public final Sink delegate() {
+ return delegate;
+ }
+
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ delegate.write(source, byteCount);
+ }
+
+ @Override public void flush() throws IOException {
+ delegate.flush();
+ }
+
+ @Override public Timeout timeout() {
+ return delegate.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ delegate.close();
+ }
+
+ @Override public String toString() {
+ return getClass().getSimpleName() + "(" + delegate.toString() + ")";
+ }
+}
diff --git a/okio/okio/src/main/java/okio/ForwardingSource.java b/okio/okio/src/main/java/okio/ForwardingSource.java
new file mode 100644
index 0000000..0cd940e
--- /dev/null
+++ b/okio/okio/src/main/java/okio/ForwardingSource.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+
+/** A {@link Source} which forwards calls to another. Useful for subclassing. */
+public abstract class ForwardingSource implements Source {
+ private final Source delegate;
+
+ public ForwardingSource(Source delegate) {
+ if (delegate == null) throw new IllegalArgumentException("delegate == null");
+ this.delegate = delegate;
+ }
+
+ /** {@link Source} to which this instance is delegating. */
+ public final Source delegate() {
+ return delegate;
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ return delegate.read(sink, byteCount);
+ }
+
+ @Override public Timeout timeout() {
+ return delegate.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ delegate.close();
+ }
+
+ @Override public String toString() {
+ return getClass().getSimpleName() + "(" + delegate.toString() + ")";
+ }
+}
diff --git a/okio/okio/src/main/java/okio/GzipSink.java b/okio/okio/src/main/java/okio/GzipSink.java
new file mode 100644
index 0000000..1a1b760
--- /dev/null
+++ b/okio/okio/src/main/java/okio/GzipSink.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+import java.util.zip.CRC32;
+import java.util.zip.Deflater;
+
+import static java.util.zip.Deflater.DEFAULT_COMPRESSION;
+
+/**
+ * A sink that uses <a href="http://www.ietf.org/rfc/rfc1952.txt">GZIP</a> to
+ * compress written data to another sink.
+ *
+ * <h3>Sync flush</h3>
+ * Aggressive flushing of this stream may result in reduced compression. Each
+ * call to {@link #flush} immediately compresses all currently-buffered data;
+ * this early compression may be less effective than compression performed
+ * without flushing.
+ *
+ * <p>This is equivalent to using {@link Deflater} with the sync flush option.
+ * This class does not offer any partial flush mechanism. For best performance,
+ * only call {@link #flush} when application behavior requires it.
+ */
+public final class GzipSink implements Sink {
+ /** Sink into which the GZIP format is written. */
+ private final BufferedSink sink;
+
+ /** The deflater used to compress the body. */
+ private final Deflater deflater;
+
+ /**
+ * The deflater sink takes care of moving data between decompressed source and
+ * compressed sink buffers.
+ */
+ private final DeflaterSink deflaterSink;
+
+ private boolean closed;
+
+ /** Checksum calculated for the compressed body. */
+ private final CRC32 crc = new CRC32();
+
+ public GzipSink(Sink sink) {
+ if (sink == null) throw new IllegalArgumentException("sink == null");
+ this.deflater = new Deflater(DEFAULT_COMPRESSION, true /* No wrap */);
+ this.sink = Okio.buffer(sink);
+ this.deflaterSink = new DeflaterSink(this.sink, deflater);
+
+ writeHeader();
+ }
+
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ if (byteCount == 0) return;
+
+ updateCrc(source, byteCount);
+ deflaterSink.write(source, byteCount);
+ }
+
+ @Override public void flush() throws IOException {
+ deflaterSink.flush();
+ }
+
+ @Override public Timeout timeout() {
+ return sink.timeout();
+ }
+
+ @Override public void close() throws IOException {
+ if (closed) return;
+
+ // This method delegates to the DeflaterSink for finishing the deflate process
+ // but keeps responsibility for releasing the deflater's resources. This is
+ // necessary because writeFooter needs to query the proccessed byte count which
+ // only works when the defalter is still open.
+
+ Throwable thrown = null;
+ try {
+ deflaterSink.finishDeflate();
+ writeFooter();
+ } catch (Throwable e) {
+ thrown = e;
+ }
+
+ try {
+ deflater.end();
+ } catch (Throwable e) {
+ if (thrown == null) thrown = e;
+ }
+
+ try {
+ sink.close();
+ } catch (Throwable e) {
+ if (thrown == null) thrown = e;
+ }
+ closed = true;
+
+ if (thrown != null) Util.sneakyRethrow(thrown);
+ }
+
+ private void writeHeader() {
+ // Write the Gzip header directly into the buffer for the sink to avoid handling IOException.
+ Buffer buffer = this.sink.buffer();
+ buffer.writeShort(0x1f8b); // Two-byte Gzip ID.
+ buffer.writeByte(0x08); // 8 == Deflate compression method.
+ buffer.writeByte(0x00); // No flags.
+ buffer.writeInt(0x00); // No modification time.
+ buffer.writeByte(0x00); // No extra flags.
+ buffer.writeByte(0x00); // No OS.
+ }
+
+ private void writeFooter() throws IOException {
+ sink.writeIntLe((int) crc.getValue()); // CRC of original data.
+ sink.writeIntLe(deflater.getTotalIn()); // Length of original data.
+ }
+
+ /** Updates the CRC with the given bytes. */
+ private void updateCrc(Buffer buffer, long byteCount) {
+ for (Segment head = buffer.head; byteCount > 0; head = head.next) {
+ int segmentLength = (int) Math.min(byteCount, head.limit - head.pos);
+ crc.update(head.data, head.pos, segmentLength);
+ byteCount -= segmentLength;
+ }
+ }
+}
diff --git a/okio/src/main/java/okio/GzipSource.java b/okio/okio/src/main/java/okio/GzipSource.java
similarity index 87%
rename from okio/src/main/java/okio/GzipSource.java
rename to okio/okio/src/main/java/okio/GzipSource.java
index eae3a16..91e7c00 100644
--- a/okio/src/main/java/okio/GzipSource.java
+++ b/okio/okio/src/main/java/okio/GzipSource.java
@@ -20,6 +20,10 @@
import java.util.zip.CRC32;
import java.util.zip.Inflater;
+/**
+ * A source that uses <a href="http://www.ietf.org/rfc/rfc1952.txt">GZIP</a> to
+ * decompress data read from another source.
+ */
public final class GzipSource implements Source {
private static final byte FHCRC = 1;
private static final byte FEXTRA = 2;
@@ -53,13 +57,14 @@
/** Checksum used to check both the GZIP header and decompressed body. */
private final CRC32 crc = new CRC32();
- public GzipSource(Source source) throws IOException {
+ public GzipSource(Source source) {
+ if (source == null) throw new IllegalArgumentException("source == null");
this.inflater = new Inflater(true);
this.source = Okio.buffer(source);
this.inflaterSource = new InflaterSource(this.source, inflater);
}
- @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (byteCount == 0) return 0;
@@ -169,9 +174,8 @@
checkEqual("ISIZE", source.readIntLe(), inflater.getTotalOut());
}
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
+ @Override public Timeout timeout() {
+ return source.timeout();
}
@Override public void close() throws IOException {
@@ -179,15 +183,20 @@
}
/** Updates the CRC with the given bytes. */
- private void updateCrc(OkBuffer buffer, long offset, long byteCount) {
- for (Segment s = buffer.head; byteCount > 0; s = s.next) {
- int segmentByteCount = s.limit - s.pos;
- if (offset < segmentByteCount) {
- int toUpdate = (int) Math.min(byteCount, segmentByteCount - offset);
- crc.update(s.data, (int) (s.pos + offset), toUpdate);
- byteCount -= toUpdate;
- }
- offset -= segmentByteCount; // Track the offset of the current segment.
+ private void updateCrc(Buffer buffer, long offset, long byteCount) {
+ // Skip segments that we aren't checksumming.
+ Segment s = buffer.head;
+ for (; offset >= (s.limit - s.pos); s = s.next) {
+ offset -= (s.limit - s.pos);
+ }
+
+ // Checksum one segment at a time.
+ for (; byteCount > 0; s = s.next) {
+ int pos = (int) (s.pos + offset);
+ int toUpdate = (int) Math.min(s.limit - pos, byteCount);
+ crc.update(s.data, pos, toUpdate);
+ byteCount -= toUpdate;
+ offset = 0;
}
}
diff --git a/okio/src/main/java/okio/InflaterSource.java b/okio/okio/src/main/java/okio/InflaterSource.java
similarity index 95%
rename from okio/src/main/java/okio/InflaterSource.java
rename to okio/okio/src/main/java/okio/InflaterSource.java
index c86c995..7363017 100644
--- a/okio/src/main/java/okio/InflaterSource.java
+++ b/okio/okio/src/main/java/okio/InflaterSource.java
@@ -53,7 +53,7 @@
}
@Override public long read(
- OkBuffer sink, long byteCount) throws IOException {
+ Buffer sink, long byteCount) throws IOException {
if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
if (closed) throw new IllegalStateException("closed");
if (byteCount == 0) return 0;
@@ -110,9 +110,8 @@
source.skip(toRelease);
}
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
+ @Override public Timeout timeout() {
+ return source.timeout();
}
@Override public void close() throws IOException {
diff --git a/okio/okio/src/main/java/okio/Okio.java b/okio/okio/src/main/java/okio/Okio.java
new file mode 100644
index 0000000..62a040f
--- /dev/null
+++ b/okio/okio/src/main/java/okio/Okio.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static okio.Util.checkOffsetAndCount;
+
+/** Essential APIs for working with Okio. */
+public final class Okio {
+ private static final Logger logger = Logger.getLogger(Okio.class.getName());
+
+ private Okio() {
+ }
+
+ /**
+ * Returns a new source that buffers reads from {@code source}. The returned
+ * source will perform bulk reads into its in-memory buffer. Use this wherever
+ * you read a source to get an ergonomic and efficient access to data.
+ */
+ public static BufferedSource buffer(Source source) {
+ if (source == null) throw new IllegalArgumentException("source == null");
+ return new RealBufferedSource(source);
+ }
+
+ /**
+ * Returns a new sink that buffers writes to {@code sink}. The returned sink
+ * will batch writes to {@code sink}. Use this wherever you write to a sink to
+ * get an ergonomic and efficient access to data.
+ */
+ public static BufferedSink buffer(Sink sink) {
+ if (sink == null) throw new IllegalArgumentException("sink == null");
+ return new RealBufferedSink(sink);
+ }
+
+ /** Returns a sink that writes to {@code out}. */
+ public static Sink sink(final OutputStream out) {
+ return sink(out, new Timeout());
+ }
+
+ private static Sink sink(final OutputStream out, final Timeout timeout) {
+ if (out == null) throw new IllegalArgumentException("out == null");
+ if (timeout == null) throw new IllegalArgumentException("timeout == null");
+
+ return new Sink() {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ checkOffsetAndCount(source.size, 0, byteCount);
+ while (byteCount > 0) {
+ timeout.throwIfReached();
+ Segment head = source.head;
+ int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
+ out.write(head.data, head.pos, toCopy);
+
+ head.pos += toCopy;
+ byteCount -= toCopy;
+ source.size -= toCopy;
+
+ if (head.pos == head.limit) {
+ source.head = head.pop();
+ SegmentPool.INSTANCE.recycle(head);
+ }
+ }
+ }
+
+ @Override public void flush() throws IOException {
+ out.flush();
+ }
+
+ @Override public void close() throws IOException {
+ out.close();
+ }
+
+ @Override public Timeout timeout() {
+ return timeout;
+ }
+
+ @Override public String toString() {
+ return "sink(" + out + ")";
+ }
+ };
+ }
+
+ /**
+ * Returns a sink that writes to {@code socket}. Prefer this over {@link
+ * #sink(OutputStream)} because this method honors timeouts. When the socket
+ * write times out, the socket is asynchronously closed by a watchdog thread.
+ */
+ public static Sink sink(final Socket socket) throws IOException {
+ if (socket == null) throw new IllegalArgumentException("socket == null");
+ AsyncTimeout timeout = timeout(socket);
+ Sink sink = sink(socket.getOutputStream(), timeout);
+ return timeout.sink(sink);
+ }
+
+ /** Returns a source that reads from {@code in}. */
+ public static Source source(final InputStream in) {
+ return source(in, new Timeout());
+ }
+
+ private static Source source(final InputStream in, final Timeout timeout) {
+ if (in == null) throw new IllegalArgumentException("in == null");
+ if (timeout == null) throw new IllegalArgumentException("timeout == null");
+
+ return new Source() {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ timeout.throwIfReached();
+ Segment tail = sink.writableSegment(1);
+ int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
+ int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
+ if (bytesRead == -1) return -1;
+ tail.limit += bytesRead;
+ sink.size += bytesRead;
+ return bytesRead;
+ }
+
+ @Override public void close() throws IOException {
+ in.close();
+ }
+
+ @Override public Timeout timeout() {
+ return timeout;
+ }
+
+ @Override public String toString() {
+ return "source(" + in + ")";
+ }
+ };
+ }
+
+ /** Returns a source that reads from {@code file}. */
+ public static Source source(File file) throws FileNotFoundException {
+ if (file == null) throw new IllegalArgumentException("file == null");
+ return source(new FileInputStream(file));
+ }
+
+ // ANDROID-BEGIN
+ // /** Returns a source that reads from {@code path}. */
+ // @IgnoreJRERequirement // Should only be invoked on Java 7+.
+ // public static Source source(Path path, OpenOption... options) throws IOException {
+ // if (path == null) throw new IllegalArgumentException("path == null");
+ // return source(Files.newInputStream(path, options));
+ // }
+ // ANDROID-END
+
+ /** Returns a sink that writes to {@code file}. */
+ public static Sink sink(File file) throws FileNotFoundException {
+ if (file == null) throw new IllegalArgumentException("file == null");
+ return sink(new FileOutputStream(file));
+ }
+
+ /** Returns a sink that appends to {@code file}. */
+ public static Sink appendingSink(File file) throws FileNotFoundException {
+ if (file == null) throw new IllegalArgumentException("file == null");
+ return sink(new FileOutputStream(file, true));
+ }
+
+ // ANDROID-BEGIN
+ // /** Returns a sink that writes to {@code path}. */
+ // @IgnoreJRERequirement // Should only be invoked on Java 7+.
+ // public static Sink sink(Path path, OpenOption... options) throws IOException {
+ // if (path == null) throw new IllegalArgumentException("path == null");
+ // return sink(Files.newOutputStream(path, options));
+ // }
+ // ANDROID-END
+
+ /**
+ * Returns a source that reads from {@code socket}. Prefer this over {@link
+ * #source(InputStream)} because this method honors timeouts. When the socket
+ * read times out, the socket is asynchronously closed by a watchdog thread.
+ */
+ public static Source source(final Socket socket) throws IOException {
+ if (socket == null) throw new IllegalArgumentException("socket == null");
+ AsyncTimeout timeout = timeout(socket);
+ Source source = source(socket.getInputStream(), timeout);
+ return timeout.source(source);
+ }
+
+ private static AsyncTimeout timeout(final Socket socket) {
+ return new AsyncTimeout() {
+ @Override protected void timedOut() {
+ try {
+ socket.close();
+ } catch (Exception e) {
+ logger.log(Level.WARNING, "Failed to close timed out socket " + socket, e);
+ }
+ }
+ };
+ }
+}
diff --git a/okio/src/main/java/okio/RealBufferedSink.java b/okio/okio/src/main/java/okio/RealBufferedSink.java
similarity index 80%
rename from okio/src/main/java/okio/RealBufferedSink.java
rename to okio/okio/src/main/java/okio/RealBufferedSink.java
index 74454c6..6a8ba88 100644
--- a/okio/src/main/java/okio/RealBufferedSink.java
+++ b/okio/okio/src/main/java/okio/RealBufferedSink.java
@@ -17,27 +17,28 @@
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.charset.Charset;
final class RealBufferedSink implements BufferedSink {
- public final OkBuffer buffer;
+ public final Buffer buffer;
public final Sink sink;
private boolean closed;
- public RealBufferedSink(Sink sink, OkBuffer buffer) {
+ public RealBufferedSink(Sink sink, Buffer buffer) {
if (sink == null) throw new IllegalArgumentException("sink == null");
this.buffer = buffer;
this.sink = sink;
}
public RealBufferedSink(Sink sink) {
- this(sink, new OkBuffer());
+ this(sink, new Buffer());
}
- @Override public OkBuffer buffer() {
+ @Override public Buffer buffer() {
return buffer;
}
- @Override public void write(OkBuffer source, long byteCount)
+ @Override public void write(Buffer source, long byteCount)
throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.write(source, byteCount);
@@ -56,6 +57,12 @@
return emitCompleteSegments();
}
+ @Override public BufferedSink writeString(String string, Charset charset) throws IOException {
+ if (closed) throw new IllegalStateException("closed");
+ buffer.writeString(string, charset);
+ return emitCompleteSegments();
+ }
+
@Override public BufferedSink write(byte[] source) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.write(source);
@@ -68,6 +75,23 @@
return emitCompleteSegments();
}
+ @Override public long writeAll(Source source) throws IOException {
+ if (source == null) throw new IllegalArgumentException("source == null");
+ long totalBytesRead = 0;
+ for (long readCount; (readCount = source.read(buffer, Segment.SIZE)) != -1; ) {
+ totalBytesRead += readCount;
+ emitCompleteSegments();
+ }
+ return totalBytesRead;
+ }
+
+ @Override public BufferedSink write(Source source, long byteCount) throws IOException {
+ if (byteCount > 0) {
+ source.read(buffer, byteCount);
+ }
+ return this;
+ }
+
@Override public BufferedSink writeByte(int b) throws IOException {
if (closed) throw new IllegalStateException("closed");
buffer.writeByte(b);
@@ -117,6 +141,13 @@
return this;
}
+ @Override public BufferedSink emit() throws IOException {
+ if (closed) throw new IllegalStateException("closed");
+ long byteCount = buffer.size();
+ if (byteCount > 0) sink.write(buffer, byteCount);
+ return this;
+ }
+
@Override public OutputStream outputStream() {
return new OutputStream() {
@Override public void write(int b) throws IOException {
@@ -180,9 +211,8 @@
if (thrown != null) Util.sneakyRethrow(thrown);
}
- @Override public Sink deadline(Deadline deadline) {
- sink.deadline(deadline);
- return this;
+ @Override public Timeout timeout() {
+ return sink.timeout();
}
@Override public String toString() {
diff --git a/okio/okio/src/main/java/okio/RealBufferedSource.java b/okio/okio/src/main/java/okio/RealBufferedSource.java
new file mode 100644
index 0000000..5e42aad
--- /dev/null
+++ b/okio/okio/src/main/java/okio/RealBufferedSource.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+
+import static okio.Util.checkOffsetAndCount;
+
+final class RealBufferedSource implements BufferedSource {
+ public final Buffer buffer;
+ public final Source source;
+ private boolean closed;
+
+ public RealBufferedSource(Source source, Buffer buffer) {
+ if (source == null) throw new IllegalArgumentException("source == null");
+ this.buffer = buffer;
+ this.source = source;
+ }
+
+ public RealBufferedSource(Source source) {
+ this(source, new Buffer());
+ }
+
+ @Override public Buffer buffer() {
+ return buffer;
+ }
+
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (sink == null) throw new IllegalArgumentException("sink == null");
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ if (closed) throw new IllegalStateException("closed");
+
+ if (buffer.size == 0) {
+ long read = source.read(buffer, Segment.SIZE);
+ if (read == -1) return -1;
+ }
+
+ long toRead = Math.min(byteCount, buffer.size);
+ return buffer.read(sink, toRead);
+ }
+
+ @Override public boolean exhausted() throws IOException {
+ if (closed) throw new IllegalStateException("closed");
+ return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1;
+ }
+
+ @Override public void require(long byteCount) throws IOException {
+ if (!request(byteCount)) throw new EOFException();
+ }
+
+ @Override public boolean request(long byteCount) throws IOException {
+ if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
+ if (closed) throw new IllegalStateException("closed");
+ while (buffer.size < byteCount) {
+ if (source.read(buffer, Segment.SIZE) == -1) return false;
+ }
+ return true;
+ }
+
+ @Override public byte readByte() throws IOException {
+ require(1);
+ return buffer.readByte();
+ }
+
+ @Override public ByteString readByteString() throws IOException {
+ buffer.writeAll(source);
+ return buffer.readByteString();
+ }
+
+ @Override public ByteString readByteString(long byteCount) throws IOException {
+ require(byteCount);
+ return buffer.readByteString(byteCount);
+ }
+
+ @Override public byte[] readByteArray() throws IOException {
+ buffer.writeAll(source);
+ return buffer.readByteArray();
+ }
+
+ @Override public byte[] readByteArray(long byteCount) throws IOException {
+ require(byteCount);
+ return buffer.readByteArray(byteCount);
+ }
+
+ @Override public int read(byte[] sink) throws IOException {
+ return read(sink, 0, sink.length);
+ }
+
+ @Override public void readFully(byte[] sink) throws IOException {
+ try {
+ require(sink.length);
+ } catch (EOFException e) {
+ // The underlying source is exhausted. Copy the bytes we got before rethrowing.
+ int offset = 0;
+ while (buffer.size > 0) {
+ int read = buffer.read(sink, offset, (int) buffer.size - offset);
+ if (read == -1) throw new AssertionError();
+ offset += read;
+ }
+ throw e;
+ }
+ buffer.readFully(sink);
+ }
+
+ @Override public int read(byte[] sink, int offset, int byteCount) throws IOException {
+ checkOffsetAndCount(sink.length, offset, byteCount);
+
+ if (buffer.size == 0) {
+ long read = source.read(buffer, Segment.SIZE);
+ if (read == -1) return -1;
+ }
+
+ int toRead = (int) Math.min(byteCount, buffer.size);
+ return buffer.read(sink, offset, toRead);
+ }
+
+ @Override public void readFully(Buffer sink, long byteCount) throws IOException {
+ try {
+ require(byteCount);
+ } catch (EOFException e) {
+ // The underlying source is exhausted. Copy the bytes we got before rethrowing.
+ sink.writeAll(buffer);
+ throw e;
+ }
+ buffer.readFully(sink, byteCount);
+ }
+
+ @Override public long readAll(Sink sink) throws IOException {
+ if (sink == null) throw new IllegalArgumentException("sink == null");
+
+ long totalBytesWritten = 0;
+ while (source.read(buffer, Segment.SIZE) != -1) {
+ long emitByteCount = buffer.completeSegmentByteCount();
+ if (emitByteCount > 0) {
+ totalBytesWritten += emitByteCount;
+ sink.write(buffer, emitByteCount);
+ }
+ }
+ if (buffer.size() > 0) {
+ totalBytesWritten += buffer.size();
+ sink.write(buffer, buffer.size());
+ }
+ return totalBytesWritten;
+ }
+
+ @Override public String readUtf8() throws IOException {
+ buffer.writeAll(source);
+ return buffer.readUtf8();
+ }
+
+ @Override public String readUtf8(long byteCount) throws IOException {
+ require(byteCount);
+ return buffer.readUtf8(byteCount);
+ }
+
+ @Override public String readString(Charset charset) throws IOException {
+ if (charset == null) throw new IllegalArgumentException("charset == null");
+
+ buffer.writeAll(source);
+ return buffer.readString(charset);
+ }
+
+ @Override public String readString(long byteCount, Charset charset) throws IOException {
+ require(byteCount);
+ if (charset == null) throw new IllegalArgumentException("charset == null");
+ return buffer.readString(byteCount, charset);
+ }
+
+ @Override public String readUtf8Line() throws IOException {
+ long newline = indexOf((byte) '\n');
+
+ if (newline == -1) {
+ return buffer.size != 0 ? readUtf8(buffer.size) : null;
+ }
+
+ return buffer.readUtf8Line(newline);
+ }
+
+ @Override public String readUtf8LineStrict() throws IOException {
+ long newline = indexOf((byte) '\n');
+ if (newline == -1L) {
+ Buffer data = new Buffer();
+ buffer.copyTo(data, 0, Math.min(32, buffer.size()));
+ throw new EOFException("\\n not found: size=" + buffer.size()
+ + " content=" + data.readByteString().hex() + "...");
+ }
+ return buffer.readUtf8Line(newline);
+ }
+
+ @Override public short readShort() throws IOException {
+ require(2);
+ return buffer.readShort();
+ }
+
+ @Override public short readShortLe() throws IOException {
+ require(2);
+ return buffer.readShortLe();
+ }
+
+ @Override public int readInt() throws IOException {
+ require(4);
+ return buffer.readInt();
+ }
+
+ @Override public int readIntLe() throws IOException {
+ require(4);
+ return buffer.readIntLe();
+ }
+
+ @Override public long readLong() throws IOException {
+ require(8);
+ return buffer.readLong();
+ }
+
+ @Override public long readLongLe() throws IOException {
+ require(8);
+ return buffer.readLongLe();
+ }
+
+ @Override public void skip(long byteCount) throws IOException {
+ if (closed) throw new IllegalStateException("closed");
+ while (byteCount > 0) {
+ if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) {
+ throw new EOFException();
+ }
+ long toSkip = Math.min(byteCount, buffer.size());
+ buffer.skip(toSkip);
+ byteCount -= toSkip;
+ }
+ }
+
+ @Override public long indexOf(byte b) throws IOException {
+ return indexOf(b, 0);
+ }
+
+ @Override public long indexOf(byte b, long fromIndex) throws IOException {
+ if (closed) throw new IllegalStateException("closed");
+ while (fromIndex >= buffer.size) {
+ if (source.read(buffer, Segment.SIZE) == -1) return -1L;
+ }
+ long index;
+ while ((index = buffer.indexOf(b, fromIndex)) == -1) {
+ fromIndex = buffer.size;
+ if (source.read(buffer, Segment.SIZE) == -1) return -1L;
+ }
+ return index;
+ }
+
+ @Override public long indexOfElement(ByteString targetBytes) throws IOException {
+ return indexOfElement(targetBytes, 0);
+ }
+
+ @Override public long indexOfElement(ByteString targetBytes, long fromIndex) throws IOException {
+ if (closed) throw new IllegalStateException("closed");
+ while (fromIndex >= buffer.size) {
+ if (source.read(buffer, Segment.SIZE) == -1) return -1L;
+ }
+ long index;
+ while ((index = buffer.indexOfElement(targetBytes, fromIndex)) == -1) {
+ fromIndex = buffer.size;
+ if (source.read(buffer, Segment.SIZE) == -1) return -1L;
+ }
+ return index;
+ }
+
+ @Override public InputStream inputStream() {
+ return new InputStream() {
+ @Override public int read() throws IOException {
+ if (closed) throw new IOException("closed");
+ if (buffer.size == 0) {
+ long count = source.read(buffer, Segment.SIZE);
+ if (count == -1) return -1;
+ }
+ return buffer.readByte() & 0xff;
+ }
+
+ @Override public int read(byte[] data, int offset, int byteCount) throws IOException {
+ if (closed) throw new IOException("closed");
+ checkOffsetAndCount(data.length, offset, byteCount);
+
+ if (buffer.size == 0) {
+ long count = source.read(buffer, Segment.SIZE);
+ if (count == -1) return -1;
+ }
+
+ return buffer.read(data, offset, byteCount);
+ }
+
+ @Override public int available() throws IOException {
+ if (closed) throw new IOException("closed");
+ return (int) Math.min(buffer.size, Integer.MAX_VALUE);
+ }
+
+ @Override public void close() throws IOException {
+ RealBufferedSource.this.close();
+ }
+
+ @Override public String toString() {
+ return RealBufferedSource.this + ".inputStream()";
+ }
+ };
+ }
+
+ @Override public void close() throws IOException {
+ if (closed) return;
+ closed = true;
+ source.close();
+ buffer.clear();
+ }
+
+ @Override public Timeout timeout() {
+ return source.timeout();
+ }
+
+ @Override public String toString() {
+ return "buffer(" + source + ")";
+ }
+}
diff --git a/okio/src/main/java/okio/Segment.java b/okio/okio/src/main/java/okio/Segment.java
similarity index 94%
rename from okio/src/main/java/okio/Segment.java
rename to okio/okio/src/main/java/okio/Segment.java
index 77dbee1..bd7f440 100644
--- a/okio/src/main/java/okio/Segment.java
+++ b/okio/okio/src/main/java/okio/Segment.java
@@ -16,9 +16,9 @@
package okio;
/**
- * A segment of an OkBuffer.
+ * A segment of a buffer.
*
- * <p>Each segment in an OkBuffer is a circularly-linked list node referencing
+ * <p>Each segment in a buffer is a circularly-linked list node referencing
* the following and preceding segments in the buffer.
*
* <p>Each segment in the pool is a singly-linked list node referencing the rest
@@ -74,7 +74,7 @@
* Splits this head of a circularly-linked list into two segments. The first
* segment contains the data in {@code [pos..pos+byteCount)}. The second
* segment contains the data in {@code [pos+byteCount..limit)}. This can be
- * useful when moving partial segments from one OkBuffer to another.
+ * useful when moving partial segments from one buffer to another.
*
* <p>Returns the new head of the circularly-linked list.
*/
@@ -115,7 +115,7 @@
SegmentPool.INSTANCE.recycle(this);
}
- /** Moves {@code byteCount} bytes from {@code sink} to this segment. */
+ /** Moves {@code byteCount} bytes from this segment to {@code sink}. */
// TODO: if sink has fewer bytes than this, it may be cheaper to reverse the
// direction of the copy and swap the segments!
public void writeTo(Segment sink, int byteCount) {
diff --git a/okio/src/main/java/okio/SegmentPool.java b/okio/okio/src/main/java/okio/SegmentPool.java
similarity index 100%
rename from okio/src/main/java/okio/SegmentPool.java
rename to okio/okio/src/main/java/okio/SegmentPool.java
diff --git a/okio/src/main/java/okio/Sink.java b/okio/okio/src/main/java/okio/Sink.java
similarity index 88%
rename from okio/src/main/java/okio/Sink.java
rename to okio/okio/src/main/java/okio/Sink.java
index 402aa0f..38c46de 100644
--- a/okio/src/main/java/okio/Sink.java
+++ b/okio/okio/src/main/java/okio/Sink.java
@@ -28,7 +28,7 @@
* {@link BufferedSink} which is both more efficient and more convenient. Use
* {@link Okio#buffer(Sink)} to wrap any sink with a buffer.
*
- * <p>Sinks are easy to test: just use an {@link OkBuffer} in your tests, and
+ * <p>Sinks are easy to test: just use an {@link Buffer} in your tests, and
* read from it to confirm it received the data that was expected.
*
* <h3>Comparison with OutputStream</h3>
@@ -39,7 +39,7 @@
* BufferedOutputStream} for buffering, and {@code OutputStreamWriter} for
* charset encoding. This class uses {@code BufferedSink} for all of the above.
*
- * <p>Sink is also easier to layer: there is no {@link
+ * <p>Sink is also easier to layer: there is no {@linkplain
* java.io.OutputStream#write(int) single-byte write} method that is awkward to
* implement efficiently.
*
@@ -49,16 +49,13 @@
*/
public interface Sink extends Closeable {
/** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
- void write(OkBuffer source, long byteCount) throws IOException;
+ void write(Buffer source, long byteCount) throws IOException;
/** Pushes all buffered bytes to their final destination. */
void flush() throws IOException;
- /**
- * Sets the deadline for all operations on this sink.
- * @return this sink.
- */
- Sink deadline(Deadline deadline);
+ /** Returns the timeout for this sink. */
+ Timeout timeout();
/**
* Pushes all buffered bytes to their final destination and releases the
diff --git a/okio/src/main/java/okio/Source.java b/okio/okio/src/main/java/okio/Source.java
similarity index 81%
rename from okio/src/main/java/okio/Source.java
rename to okio/okio/src/main/java/okio/Source.java
index d402bee..d0bd7f1 100644
--- a/okio/src/main/java/okio/Source.java
+++ b/okio/okio/src/main/java/okio/Source.java
@@ -28,26 +28,26 @@
* {@link BufferedSource} which is both more efficient and more convenient. Use
* {@link Okio#buffer(Source)} to wrap any source with a buffer.
*
- * <p>Sources are easy to test: just use an {@link OkBuffer} in your tests, and
+ * <p>Sources are easy to test: just use an {@link Buffer} in your tests, and
* fill it with the data your application is to read.
*
* <h3>Comparison with InputStream</h3>
* This interface is functionally equivalent to {@link java.io.InputStream}.
*
* <p>{@code InputStream} requires multiple layers when consumed data is
- * heterogeneous: a {@code DataOutputStream} for primitive values, a {@code
+ * heterogeneous: a {@code DataInputStream} for primitive values, a {@code
* BufferedInputStream} for buffering, and {@code InputStreamReader} for
* strings. This class uses {@code BufferedSource} for all of the above.
*
- * <p>Source avoids the impossible-to-implement {@link
+ * <p>Source avoids the impossible-to-implement {@linkplain
* java.io.InputStream#available available()} method. Instead callers specify
* how many bytes they {@link BufferedSource#require require}.
*
- * <p>Source omits the unsafe-to-compose {@link java.io.InputStream#mark mark
- * and reset} state that's tracked by {@code InputStream}; callers instead just
- * buffer what they need.
+ * <p>Source omits the unsafe-to-compose {@linkplain java.io.InputStream#mark
+ * mark and reset} state that's tracked by {@code InputStream}; callers instead
+ * just buffer what they need.
*
- * <p>When implementing a source, you need not worry about the {@link
+ * <p>When implementing a source, you need not worry about the {@linkplain
* java.io.InputStream#read single-byte read} method that is awkward to
* implement efficiently and that returns one of 257 possible values.
*
@@ -65,13 +65,10 @@
* them to {@code sink}. Returns the number of bytes read, or -1 if this
* source is exhausted.
*/
- long read(OkBuffer sink, long byteCount) throws IOException;
+ long read(Buffer sink, long byteCount) throws IOException;
- /**
- * Sets the deadline for all operations on this source.
- * @return this source.
- */
- Source deadline(Deadline deadline);
+ /** Returns the timeout for this source. */
+ Timeout timeout();
/**
* Closes this source and releases the resources held by this source. It is an
diff --git a/okio/okio/src/main/java/okio/Timeout.java b/okio/okio/src/main/java/okio/Timeout.java
new file mode 100644
index 0000000..ce220ec
--- /dev/null
+++ b/okio/okio/src/main/java/okio/Timeout.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A policy on how much time to spend on a task before giving up. When a task
+ * times out, it is left in an unspecified state and should be abandoned. For
+ * example, if reading from a source times out, that source should be closed and
+ * the read should be retried later. If writing to a sink times out, the same
+ * rules apply: close the sink and retry later.
+ *
+ * <h3>Timeouts and Deadlines</h3>
+ * This class offers two complementary controls to define a timeout policy.
+ *
+ * <p><strong>Timeouts</strong> specify the maximum time to wait for a single
+ * operation to complete. Timeouts are typically used to detect problems like
+ * network partitions. For example, if a remote peer doesn't return <i>any</i>
+ * data for ten seconds, we may assume that the peer is unavailable.
+ *
+ * <p><strong>Deadlines</strong> specify the maximum time to spend on a job,
+ * composed of one or more operations. Use deadlines to set an upper bound on
+ * the time invested on a job. For example, a battery-conscious app may limit
+ * how much time it spends preloading content.
+ */
+public class Timeout {
+ /**
+ * An empty timeout that neither tracks nor detects timeouts. Use this when
+ * timeouts aren't necessary, such as in implementations whose operations
+ * do not block.
+ */
+ public static final Timeout NONE = new Timeout() {
+ @Override public Timeout timeout(long timeout, TimeUnit unit) {
+ return this;
+ }
+
+ @Override public Timeout deadlineNanoTime(long deadlineNanoTime) {
+ return this;
+ }
+
+ @Override public void throwIfReached() throws IOException {
+ }
+ };
+
+ /**
+ * True if {@code deadlineNanoTime} is defined. There is no equivalent to null
+ * or 0 for {@link System#nanoTime}.
+ */
+ private boolean hasDeadline;
+ private long deadlineNanoTime;
+ private long timeoutNanos;
+
+ public Timeout() {
+ }
+
+ /**
+ * Wait at most {@code timeout} time before aborting an operation. Using a
+ * per-operation timeout means that as long as forward progress is being made,
+ * no sequence of operations will fail.
+ *
+ * <p>If {@code timeout == 0}, operations will run indefinitely. (Operating
+ * system timeouts may still apply.)
+ */
+ public Timeout timeout(long timeout, TimeUnit unit) {
+ if (timeout < 0) throw new IllegalArgumentException("timeout < 0: " + timeout);
+ if (unit == null) throw new IllegalArgumentException("unit == null");
+ this.timeoutNanos = unit.toNanos(timeout);
+ return this;
+ }
+
+ /** Returns the timeout in nanoseconds, or {@code 0} for no timeout. */
+ public long timeoutNanos() {
+ return timeoutNanos;
+ }
+
+ /** Returns true if a deadline is enabled. */
+ public boolean hasDeadline() {
+ return hasDeadline;
+ }
+
+ /**
+ * Returns the {@linkplain System#nanoTime() nano time} when the deadline will
+ * be reached.
+ *
+ * @throws IllegalStateException if no deadline is set.
+ */
+ public long deadlineNanoTime() {
+ if (!hasDeadline) throw new IllegalStateException("No deadline");
+ return deadlineNanoTime;
+ }
+
+ /**
+ * Sets the {@linkplain System#nanoTime() nano time} when the deadline will be
+ * reached. All operations must complete before this time. Use a deadline to
+ * set a maximum bound on the time spent on a sequence of operations.
+ */
+ public Timeout deadlineNanoTime(long deadlineNanoTime) {
+ this.hasDeadline = true;
+ this.deadlineNanoTime = deadlineNanoTime;
+ return this;
+ }
+
+ /** Set a deadline of now plus {@code duration} time. */
+ public final Timeout deadline(long duration, TimeUnit unit) {
+ if (duration <= 0) throw new IllegalArgumentException("duration <= 0: " + duration);
+ if (unit == null) throw new IllegalArgumentException("unit == null");
+ return deadlineNanoTime(System.nanoTime() + unit.toNanos(duration));
+ }
+
+ /** Clears the timeout. Operating system timeouts may still apply. */
+ public Timeout clearTimeout() {
+ this.timeoutNanos = 0;
+ return this;
+ }
+
+ /** Clears the deadline. */
+ public Timeout clearDeadline() {
+ this.hasDeadline = false;
+ return this;
+ }
+
+ /**
+ * Throws an {@link IOException} if the deadline has been reached or if the
+ * current thread has been interrupted. This method doesn't detect timeouts;
+ * that should be implemented to asynchronously abort an in-progress
+ * operation.
+ */
+ public void throwIfReached() throws IOException {
+ if (Thread.interrupted()) {
+ throw new InterruptedIOException();
+ }
+
+ if (hasDeadline && System.nanoTime() > deadlineNanoTime) {
+ throw new IOException("deadline reached");
+ }
+ }
+}
diff --git a/okio/src/main/java/okio/Util.java b/okio/okio/src/main/java/okio/Util.java
similarity index 87%
rename from okio/src/main/java/okio/Util.java
rename to okio/okio/src/main/java/okio/Util.java
index 4759488..60ddc8e 100644
--- a/okio/src/main/java/okio/Util.java
+++ b/okio/okio/src/main/java/okio/Util.java
@@ -24,9 +24,10 @@
private Util() {
}
- public static void checkOffsetAndCount(long arrayLength, long offset, long count) {
- if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
- throw new ArrayIndexOutOfBoundsException();
+ public static void checkOffsetAndCount(long size, long offset, long byteCount) {
+ if ((offset | byteCount) < 0 || offset > size || size - offset < byteCount) {
+ throw new ArrayIndexOutOfBoundsException(
+ String.format("size=%s offset=%s byteCount=%s", size, offset, byteCount));
}
}
diff --git a/okio/okio/src/main/java/okio/package-info.java b/okio/okio/src/main/java/okio/package-info.java
new file mode 100644
index 0000000..82296b4
--- /dev/null
+++ b/okio/okio/src/main/java/okio/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Okio complements {@link java.io} and {@link java.nio} to make it much easier to access, store,
+ * and process your data.
+ */
+package okio;
diff --git a/okio/okio/src/test/java/okio/AsyncTimeoutTest.java b/okio/okio/src/test/java/okio/AsyncTimeoutTest.java
new file mode 100644
index 0000000..bf4089e
--- /dev/null
+++ b/okio/okio/src/test/java/okio/AsyncTimeoutTest.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.TimeUnit;
+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.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * This test uses four timeouts of varying durations: 250ms, 500ms, 750ms and
+ * 1000ms, named 'a', 'b', 'c' and 'd'.
+ */
+public class AsyncTimeoutTest {
+ private final List<Timeout> timedOut = new CopyOnWriteArrayList<Timeout>();
+ private final AsyncTimeout a = new RecordingAsyncTimeout();
+ private final AsyncTimeout b = new RecordingAsyncTimeout();
+ private final AsyncTimeout c = new RecordingAsyncTimeout();
+ private final AsyncTimeout d = new RecordingAsyncTimeout();
+
+ @Before public void setUp() throws Exception {
+ a.timeout( 250, TimeUnit.MILLISECONDS);
+ b.timeout( 500, TimeUnit.MILLISECONDS);
+ c.timeout( 750, TimeUnit.MILLISECONDS);
+ d.timeout(1000, TimeUnit.MILLISECONDS);
+ }
+
+ @Test public void zeroTimeoutIsNoTimeout() throws Exception {
+ AsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.timeout(0, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(250);
+ assertFalse(timeout.exit());
+ assertTimedOut();
+ }
+
+ @Test public void singleInstanceTimedOut() throws Exception {
+ a.enter();
+ Thread.sleep(500);
+ assertTrue(a.exit());
+ assertTimedOut(a);
+ }
+
+ @Test public void singleInstanceNotTimedOut() throws Exception {
+ b.enter();
+ Thread.sleep(250);
+ b.exit();
+ assertFalse(b.exit());
+ assertTimedOut();
+ }
+
+ @Test public void instancesAddedAtEnd() throws Exception {
+ a.enter();
+ b.enter();
+ c.enter();
+ d.enter();
+ Thread.sleep(1250);
+ assertTrue(a.exit());
+ assertTrue(b.exit());
+ assertTrue(c.exit());
+ assertTrue(d.exit());
+ assertTimedOut(a, b, c, d);
+ }
+
+ @Test public void instancesAddedAtFront() throws Exception {
+ d.enter();
+ c.enter();
+ b.enter();
+ a.enter();
+ Thread.sleep(1250);
+ assertTrue(d.exit());
+ assertTrue(c.exit());
+ assertTrue(b.exit());
+ assertTrue(a.exit());
+ assertTimedOut(a, b, c, d);
+ }
+
+ @Test public void instancesRemovedAtFront() throws Exception {
+ a.enter();
+ b.enter();
+ c.enter();
+ d.enter();
+ assertFalse(a.exit());
+ assertFalse(b.exit());
+ assertFalse(c.exit());
+ assertFalse(d.exit());
+ assertTimedOut();
+ }
+
+ @Test public void instancesRemovedAtEnd() throws Exception {
+ a.enter();
+ b.enter();
+ c.enter();
+ d.enter();
+ assertFalse(d.exit());
+ assertFalse(c.exit());
+ assertFalse(b.exit());
+ assertFalse(a.exit());
+ assertTimedOut();
+ }
+
+ /** Detecting double-enters is not guaranteed. */
+ @Test public void doubleEnter() throws Exception {
+ a.enter();
+ try {
+ a.enter();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+ }
+
+ @Test public void deadlineOnly() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadline(250, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(500);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineBeforeTimeout() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadline(250, TimeUnit.MILLISECONDS);
+ timeout.timeout(750, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(500);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineAfterTimeout() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ timeout.deadline(750, TimeUnit.MILLISECONDS);
+ timeout.enter();
+ Thread.sleep(500);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineStartsBeforeEnter() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadline(500, TimeUnit.MILLISECONDS);
+ Thread.sleep(500);
+ timeout.enter();
+ Thread.sleep(250);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void deadlineInThePast() throws Exception {
+ RecordingAsyncTimeout timeout = new RecordingAsyncTimeout();
+ timeout.deadlineNanoTime(System.nanoTime() - 1);
+ timeout.enter();
+ Thread.sleep(250);
+ assertTrue(timeout.exit());
+ assertTimedOut(timeout);
+ }
+
+ @Test public void wrappedSinkTimesOut() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ try {
+ Thread.sleep(500);
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ try {
+ timeoutSink.write(null, 0);
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedSourceTimesOut() throws Exception {
+ Source source = new ForwardingSource(new Buffer()) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ try {
+ Thread.sleep(500);
+ return -1;
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Source timeoutSource = timeout.source(source);
+ try {
+ timeoutSource.read(null, 0);
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ }
+
+ @Test public void wrappedThrowsWithTimeout() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ try {
+ Thread.sleep(500);
+ throw new IOException("exception and timeout");
+ } catch (InterruptedException e) {
+ throw new AssertionError();
+ }
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ try {
+ timeoutSink.write(null, 0);
+ fail();
+ } catch (InterruptedIOException expected) {
+ assertEquals("timeout", expected.getMessage());
+ assertEquals("exception and timeout", expected.getCause().getMessage());
+ }
+ }
+
+ @Test public void wrappedThrowsWithoutTimeout() throws Exception {
+ Sink sink = new ForwardingSink(new Buffer()) {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ throw new IOException("no timeout occurred");
+ }
+ };
+ AsyncTimeout timeout = new AsyncTimeout();
+ timeout.timeout(250, TimeUnit.MILLISECONDS);
+ Sink timeoutSink = timeout.sink(sink);
+ try {
+ timeoutSink.write(null, 0);
+ fail();
+ } catch (IOException expected) {
+ assertEquals("no timeout occurred", expected.getMessage());
+ }
+ }
+
+ /** Asserts which timeouts fired, and in which order. */
+ private void assertTimedOut(Timeout... expected) {
+ assertEquals(Arrays.asList(expected), timedOut);
+ }
+
+ class RecordingAsyncTimeout extends AsyncTimeout {
+ @Override protected void timedOut() {
+ timedOut.add(this);
+ }
+ }
+}
diff --git a/okio/okio/src/test/java/okio/BufferTest.java b/okio/okio/src/test/java/okio/BufferTest.java
new file mode 100644
index 0000000..32daca4
--- /dev/null
+++ b/okio/okio/src/test/java/okio/BufferTest.java
@@ -0,0 +1,596 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import org.junit.Test;
+
+import static java.util.Arrays.asList;
+import static okio.TestUtil.repeat;
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Tests solely for the behavior of Buffer's implementation. For generic BufferedSink or
+ * BufferedSource behavior use BufferedSinkTest or BufferedSourceTest, respectively.
+ */
+public final class BufferTest {
+ @Test public void readAndWriteUtf8() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8("ab");
+ assertEquals(2, buffer.size());
+ buffer.writeUtf8("cdef");
+ assertEquals(6, buffer.size());
+ assertEquals("abcd", buffer.readUtf8(4));
+ assertEquals(2, buffer.size());
+ assertEquals("ef", buffer.readUtf8(2));
+ assertEquals(0, buffer.size());
+ try {
+ buffer.readUtf8(1);
+ fail();
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ }
+ }
+
+ @Test public void completeSegmentByteCountOnEmptyBuffer() throws Exception {
+ Buffer buffer = new Buffer();
+ assertEquals(0, buffer.completeSegmentByteCount());
+ }
+
+ @Test public void completeSegmentByteCountOnBufferWithFullSegments() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat('a', Segment.SIZE * 4));
+ assertEquals(Segment.SIZE * 4, buffer.completeSegmentByteCount());
+ }
+
+ @Test public void completeSegmentByteCountOnBufferWithIncompleteTailSegment() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat('a', Segment.SIZE * 4 - 10));
+ assertEquals(Segment.SIZE * 3, buffer.completeSegmentByteCount());
+ }
+
+ @Test public void toStringOnEmptyBuffer() throws Exception {
+ Buffer buffer = new Buffer();
+ assertEquals("Buffer[size=0]", buffer.toString());
+ }
+
+ @Test public void toStringOnSmallBufferIncludesContents() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.decodeHex("a1b2c3d4e5f61a2b3c4d5e6f10203040"));
+ assertEquals("Buffer[size=16 data=a1b2c3d4e5f61a2b3c4d5e6f10203040]", buffer.toString());
+ }
+
+ @Test public void toStringOnLargeBufferIncludesMd5() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.write(ByteString.encodeUtf8("12345678901234567"));
+ assertEquals("Buffer[size=17 md5=2c9728a2138b2f25e9f89f99bdccf8db]", buffer.toString());
+ }
+
+ @Test public void toStringOnMultipleSegmentBuffer() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat('a', 6144));
+ assertEquals("Buffer[size=6144 md5=d890021f28522533c1cc1b9b1f83ce73]", buffer.toString());
+ }
+
+ @Test public void multipleSegmentBuffers() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat('a', 1000));
+ buffer.writeUtf8(repeat('b', 2500));
+ buffer.writeUtf8(repeat('c', 5000));
+ buffer.writeUtf8(repeat('d', 10000));
+ buffer.writeUtf8(repeat('e', 25000));
+ buffer.writeUtf8(repeat('f', 50000));
+
+ assertEquals(repeat('a', 999), buffer.readUtf8(999)); // a...a
+ assertEquals("a" + repeat('b', 2500) + "c", buffer.readUtf8(2502)); // ab...bc
+ assertEquals(repeat('c', 4998), buffer.readUtf8(4998)); // c...c
+ assertEquals("c" + repeat('d', 10000) + "e", buffer.readUtf8(10002)); // cd...de
+ assertEquals(repeat('e', 24998), buffer.readUtf8(24998)); // e...e
+ assertEquals("e" + repeat('f', 50000), buffer.readUtf8(50001)); // ef...f
+ assertEquals(0, buffer.size());
+ }
+
+ @Test public void fillAndDrainPool() throws Exception {
+ Buffer buffer = new Buffer();
+
+ // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it.
+ buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+ buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+ assertEquals(0, SegmentPool.INSTANCE.byteCount);
+
+ // Recycle MAX_SIZE segments. They're all in the pool.
+ buffer.readByteString(SegmentPool.MAX_SIZE);
+ assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
+
+ // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected.
+ buffer.readByteString(SegmentPool.MAX_SIZE);
+ assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
+
+ // Take MAX_SIZE segments to drain the pool.
+ buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+ assertEquals(0, SegmentPool.INSTANCE.byteCount);
+
+ // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated.
+ buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
+ assertEquals(0, SegmentPool.INSTANCE.byteCount);
+ }
+
+ @Test public void moveBytesBetweenBuffersShareSegment() throws Exception {
+ int size = (Segment.SIZE / 2) - 1;
+ List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
+ assertEquals(asList(size * 2), segmentSizes);
+ }
+
+ @Test public void moveBytesBetweenBuffersReassignSegment() throws Exception {
+ int size = (Segment.SIZE / 2) + 1;
+ List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
+ assertEquals(asList(size, size), segmentSizes);
+ }
+
+ @Test public void moveBytesBetweenBuffersMultipleSegments() throws Exception {
+ int size = 3 * Segment.SIZE + 1;
+ List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
+ assertEquals(asList(Segment.SIZE, Segment.SIZE, Segment.SIZE, 1,
+ Segment.SIZE, Segment.SIZE, Segment.SIZE, 1), segmentSizes);
+ }
+
+ private List<Integer> moveBytesBetweenBuffers(String... contents) throws IOException {
+ StringBuilder expected = new StringBuilder();
+ Buffer buffer = new Buffer();
+ for (String s : contents) {
+ Buffer source = new Buffer();
+ source.writeUtf8(s);
+ buffer.writeAll(source);
+ expected.append(s);
+ }
+ List<Integer> segmentSizes = buffer.segmentSizes();
+ assertEquals(expected.toString(), buffer.readUtf8(expected.length()));
+ return segmentSizes;
+ }
+
+ /** The big part of source's first segment is being moved. */
+ @Test public void writeSplitSourceBufferLeft() throws Exception {
+ int writeSize = Segment.SIZE / 2 + 1;
+
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('b', Segment.SIZE - 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('a', Segment.SIZE * 2));
+ sink.write(source, writeSize);
+
+ assertEquals(asList(Segment.SIZE - 10, writeSize), sink.segmentSizes());
+ assertEquals(asList(Segment.SIZE - writeSize, Segment.SIZE), source.segmentSizes());
+ }
+
+ /** The big part of source's first segment is staying put. */
+ @Test public void writeSplitSourceBufferRight() throws Exception {
+ int writeSize = Segment.SIZE / 2 - 1;
+
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('b', Segment.SIZE - 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('a', Segment.SIZE * 2));
+ sink.write(source, writeSize);
+
+ assertEquals(asList(Segment.SIZE - 10, writeSize), sink.segmentSizes());
+ assertEquals(asList(Segment.SIZE - writeSize, Segment.SIZE), source.segmentSizes());
+ }
+
+ @Test public void writePrefixDoesntSplit() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('b', 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('a', Segment.SIZE * 2));
+ sink.write(source, 20);
+
+ assertEquals(asList(30), sink.segmentSizes());
+ assertEquals(asList(Segment.SIZE - 20, Segment.SIZE), source.segmentSizes());
+ assertEquals(30, sink.size());
+ assertEquals(Segment.SIZE * 2 - 20, source.size());
+ }
+
+ @Test public void writePrefixDoesntSplitButRequiresCompact() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('b', Segment.SIZE - 10)); // limit = size - 10
+ sink.readUtf8(Segment.SIZE - 20); // pos = size = 20
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('a', Segment.SIZE * 2));
+ sink.write(source, 20);
+
+ assertEquals(asList(30), sink.segmentSizes());
+ assertEquals(asList(Segment.SIZE - 20, Segment.SIZE), source.segmentSizes());
+ assertEquals(30, sink.size());
+ assertEquals(Segment.SIZE * 2 - 20, source.size());
+ }
+
+ @Test public void copyToSpanningSegments() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('a', Segment.SIZE * 2));
+ source.writeUtf8(repeat('b', Segment.SIZE * 2));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ source.copyTo(out, 10, Segment.SIZE * 3);
+
+ assertEquals(repeat('a', Segment.SIZE * 2 - 10) + repeat('b', Segment.SIZE + 10),
+ out.toString());
+ assertEquals(repeat('a', Segment.SIZE * 2) + repeat('b', Segment.SIZE * 2),
+ source.readUtf8(Segment.SIZE * 4));
+ }
+
+ @Test public void copyToStream() throws Exception {
+ Buffer buffer = new Buffer().writeUtf8("hello, world!");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buffer.copyTo(out);
+ String outString = new String(out.toByteArray(), UTF_8);
+ assertEquals("hello, world!", outString);
+ assertEquals("hello, world!", buffer.readUtf8());
+ }
+
+ @Test public void writeToSpanningSegments() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8(repeat('a', Segment.SIZE * 2));
+ buffer.writeUtf8(repeat('b', Segment.SIZE * 2));
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buffer.skip(10);
+ buffer.writeTo(out, Segment.SIZE * 3);
+
+ assertEquals(repeat('a', Segment.SIZE * 2 - 10) + repeat('b', Segment.SIZE + 10),
+ out.toString());
+ assertEquals(repeat('b', Segment.SIZE - 10), buffer.readUtf8(buffer.size));
+ }
+
+ @Test public void writeToStream() throws Exception {
+ Buffer buffer = new Buffer().writeUtf8("hello, world!");
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ buffer.writeTo(out);
+ String outString = new String(out.toByteArray(), UTF_8);
+ assertEquals("hello, world!", outString);
+ assertEquals(0, buffer.size());
+ }
+
+ @Test public void readFromStream() throws Exception {
+ InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8));
+ Buffer buffer = new Buffer();
+ buffer.readFrom(in);
+ String out = buffer.readUtf8();
+ assertEquals("hello, world!", out);
+ }
+
+ @Test public void readFromSpanningSegments() throws Exception {
+ InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8));
+ Buffer buffer = new Buffer().writeUtf8(repeat('a', Segment.SIZE - 10));
+ buffer.readFrom(in);
+ String out = buffer.readUtf8();
+ assertEquals(repeat('a', Segment.SIZE - 10) + "hello, world!", out);
+ }
+
+ @Test public void readFromStreamWithCount() throws Exception {
+ InputStream in = new ByteArrayInputStream("hello, world!".getBytes(UTF_8));
+ Buffer buffer = new Buffer();
+ buffer.readFrom(in, 10);
+ String out = buffer.readUtf8();
+ assertEquals("hello, wor", out);
+ }
+
+ @Test public void moveAllRequestedBytesWithRead() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('a', 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('b', 15));
+
+ assertEquals(10, source.read(sink, 10));
+ assertEquals(20, sink.size());
+ assertEquals(5, source.size());
+ assertEquals(repeat('a', 10) + repeat('b', 10), sink.readUtf8(20));
+ }
+
+ @Test public void moveFewerThanRequestedBytesWithRead() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('a', 10));
+
+ Buffer source = new Buffer();
+ source.writeUtf8(repeat('b', 20));
+
+ assertEquals(20, source.read(sink, 25));
+ assertEquals(30, sink.size());
+ assertEquals(0, source.size());
+ assertEquals(repeat('a', 10) + repeat('b', 20), sink.readUtf8(30));
+ }
+
+ @Test public void indexOfWithOffset() throws Exception {
+ Buffer buffer = new Buffer();
+ int halfSegment = Segment.SIZE / 2;
+ buffer.writeUtf8(repeat('a', halfSegment));
+ buffer.writeUtf8(repeat('b', halfSegment));
+ buffer.writeUtf8(repeat('c', halfSegment));
+ buffer.writeUtf8(repeat('d', halfSegment));
+ assertEquals(0, buffer.indexOf((byte) 'a', 0));
+ assertEquals(halfSegment - 1, buffer.indexOf((byte) 'a', halfSegment - 1));
+ assertEquals(halfSegment, buffer.indexOf((byte) 'b', halfSegment - 1));
+ assertEquals(halfSegment * 2, buffer.indexOf((byte) 'c', halfSegment - 1));
+ assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment - 1));
+ assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 2));
+ assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 3));
+ assertEquals(halfSegment * 4 - 1, buffer.indexOf((byte) 'd', halfSegment * 4 - 1));
+ }
+
+ @Test public void byteAt() throws Exception {
+ Buffer buffer = new Buffer();
+ buffer.writeUtf8("a");
+ buffer.writeUtf8(repeat('b', Segment.SIZE));
+ buffer.writeUtf8("c");
+ assertEquals('a', buffer.getByte(0));
+ assertEquals('a', buffer.getByte(0)); // getByte doesn't mutate!
+ assertEquals('c', buffer.getByte(buffer.size - 1));
+ assertEquals('b', buffer.getByte(buffer.size - 2));
+ assertEquals('b', buffer.getByte(buffer.size - 3));
+ }
+
+ @Test public void getByteOfEmptyBuffer() throws Exception {
+ Buffer buffer = new Buffer();
+ try {
+ buffer.getByte(0);
+ fail();
+ } catch (IndexOutOfBoundsException expected) {
+ }
+ }
+
+ @Test public void writePrefixToEmptyBuffer() throws IOException {
+ Buffer sink = new Buffer();
+ Buffer source = new Buffer();
+ source.writeUtf8("abcd");
+ sink.write(source, 2);
+ assertEquals("ab", sink.readUtf8(2));
+ }
+
+ @Test public void cloneDoesNotObserveWritesToOriginal() throws Exception {
+ Buffer original = new Buffer();
+ Buffer clone = original.clone();
+ original.writeUtf8("abc");
+ assertEquals(0, clone.size());
+ }
+
+ @Test public void cloneDoesNotObserveReadsFromOriginal() throws Exception {
+ Buffer original = new Buffer();
+ original.writeUtf8("abc");
+ Buffer clone = original.clone();
+ assertEquals("abc", original.readUtf8(3));
+ assertEquals(3, clone.size());
+ assertEquals("ab", clone.readUtf8(2));
+ }
+
+ @Test public void originalDoesNotObserveWritesToClone() throws Exception {
+ Buffer original = new Buffer();
+ Buffer clone = original.clone();
+ clone.writeUtf8("abc");
+ assertEquals(0, original.size());
+ }
+
+ @Test public void originalDoesNotObserveReadsFromClone() throws Exception {
+ Buffer original = new Buffer();
+ original.writeUtf8("abc");
+ Buffer clone = original.clone();
+ assertEquals("abc", clone.readUtf8(3));
+ assertEquals(3, original.size());
+ assertEquals("ab", original.readUtf8(2));
+ }
+
+ @Test public void cloneMultipleSegments() throws Exception {
+ Buffer original = new Buffer();
+ original.writeUtf8(repeat('a', Segment.SIZE * 3));
+ Buffer clone = original.clone();
+ original.writeUtf8(repeat('b', Segment.SIZE * 3));
+ clone.writeUtf8(repeat('c', Segment.SIZE * 3));
+
+ assertEquals(repeat('a', Segment.SIZE * 3) + repeat('b', Segment.SIZE * 3),
+ original.readUtf8(Segment.SIZE * 6));
+ assertEquals(repeat('a', Segment.SIZE * 3) + repeat('c', Segment.SIZE * 3),
+ clone.readUtf8(Segment.SIZE * 6));
+ }
+
+ @Test public void equalsAndHashCodeEmpty() throws Exception {
+ Buffer a = new Buffer();
+ Buffer b = new Buffer();
+ assertTrue(a.equals(b));
+ assertTrue(a.hashCode() == b.hashCode());
+ }
+
+ @Test public void equalsAndHashCode() throws Exception {
+ Buffer a = new Buffer().writeUtf8("dog");
+ Buffer b = new Buffer().writeUtf8("hotdog");
+ assertFalse(a.equals(b));
+ assertFalse(a.hashCode() == b.hashCode());
+
+ b.readUtf8(3); // Leaves b containing 'dog'.
+ assertTrue(a.equals(b));
+ assertTrue(a.hashCode() == b.hashCode());
+ }
+
+ @Test public void equalsAndHashCodeSpanningSegments() throws Exception {
+ byte[] data = new byte[1024 * 1024];
+ Random dice = new Random(0);
+ dice.nextBytes(data);
+
+ Buffer a = bufferWithRandomSegmentLayout(dice, data);
+ Buffer b = bufferWithRandomSegmentLayout(dice, data);
+ assertTrue(a.equals(b));
+ assertTrue(a.hashCode() == b.hashCode());
+
+ data[data.length / 2]++; // Change a single byte.
+ Buffer c = bufferWithRandomSegmentLayout(dice, data);
+ assertFalse(a.equals(c));
+ assertFalse(a.hashCode() == c.hashCode());
+ }
+
+ @Test public void bufferInputStreamByteByByte() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("abc");
+
+ InputStream in = source.inputStream();
+ assertEquals(3, in.available());
+ assertEquals('a', in.read());
+ assertEquals('b', in.read());
+ assertEquals('c', in.read());
+ assertEquals(-1, in.read());
+ assertEquals(0, in.available());
+ }
+
+ @Test public void bufferInputStreamBulkReads() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("abc");
+
+ byte[] byteArray = new byte[4];
+
+ Arrays.fill(byteArray, (byte) -5);
+ InputStream in = source.inputStream();
+ assertEquals(3, in.read(byteArray));
+ assertEquals("[97, 98, 99, -5]", Arrays.toString(byteArray));
+
+ Arrays.fill(byteArray, (byte) -7);
+ assertEquals(-1, in.read(byteArray));
+ assertEquals("[-7, -7, -7, -7]", Arrays.toString(byteArray));
+ }
+
+ /**
+ * When writing data that's already buffered, there's no reason to page the
+ * data by segment.
+ */
+ @Test public void readAllWritesAllSegmentsAtOnce() throws Exception {
+ Buffer write1 = new Buffer().writeUtf8(""
+ + TestUtil.repeat('a', Segment.SIZE)
+ + TestUtil.repeat('b', Segment.SIZE)
+ + TestUtil.repeat('c', Segment.SIZE));
+
+ Buffer source = new Buffer().writeUtf8(""
+ + TestUtil.repeat('a', Segment.SIZE)
+ + TestUtil.repeat('b', Segment.SIZE)
+ + TestUtil.repeat('c', Segment.SIZE));
+
+ MockSink mockSink = new MockSink();
+
+ assertEquals(Segment.SIZE * 3, source.readAll(mockSink));
+ assertEquals(0, source.size());
+ mockSink.assertLog("write(" + write1 + ", " + write1.size() + ")");
+ }
+
+ @Test public void writeAllMultipleSegments() throws Exception {
+ Buffer source = new Buffer().writeUtf8(TestUtil.repeat('a', Segment.SIZE * 3));
+ Buffer sink = new Buffer();
+
+ assertEquals(Segment.SIZE * 3, sink.writeAll(source));
+ assertEquals(0, source.size());
+ assertEquals(TestUtil.repeat('a', Segment.SIZE * 3), sink.readUtf8());
+ }
+
+ @Test public void copyTo() throws Exception {
+ Buffer source = new Buffer();
+ source.writeUtf8("party");
+
+ Buffer target = new Buffer();
+ source.copyTo(target, 1, 3);
+
+ assertEquals("art", target.readUtf8());
+ assertEquals("party", source.readUtf8());
+ }
+
+ @Test public void copyToOnSegmentBoundary() throws Exception {
+ String as = repeat('a', Segment.SIZE);
+ String bs = repeat('b', Segment.SIZE);
+ String cs = repeat('c', Segment.SIZE);
+ String ds = repeat('d', Segment.SIZE);
+
+ Buffer source = new Buffer();
+ source.writeUtf8(as);
+ source.writeUtf8(bs);
+ source.writeUtf8(cs);
+
+ Buffer target = new Buffer();
+ target.writeUtf8(ds);
+
+ source.copyTo(target, as.length(), bs.length() + cs.length());
+ assertEquals(ds + bs + cs, target.readUtf8());
+ }
+
+ @Test public void copyToOffSegmentBoundary() throws Exception {
+ String as = repeat('a', Segment.SIZE - 1);
+ String bs = repeat('b', Segment.SIZE + 2);
+ String cs = repeat('c', Segment.SIZE - 4);
+ String ds = repeat('d', Segment.SIZE + 8);
+
+ Buffer source = new Buffer();
+ source.writeUtf8(as);
+ source.writeUtf8(bs);
+ source.writeUtf8(cs);
+
+ Buffer target = new Buffer();
+ target.writeUtf8(ds);
+
+ source.copyTo(target, as.length(), bs.length() + cs.length());
+ assertEquals(ds + bs + cs, target.readUtf8());
+ }
+
+ @Test public void copyToSourceAndTargetCanBeTheSame() throws Exception {
+ String as = repeat('a', Segment.SIZE);
+ String bs = repeat('b', Segment.SIZE);
+
+ Buffer source = new Buffer();
+ source.writeUtf8(as);
+ source.writeUtf8(bs);
+
+ source.copyTo(source, 0, source.size());
+ assertEquals(as + bs + as + bs, source.readUtf8());
+ }
+
+ /**
+ * Returns a new buffer containing the data in {@code data}, and a segment
+ * layout determined by {@code dice}.
+ */
+ private Buffer bufferWithRandomSegmentLayout(Random dice, byte[] data) throws IOException {
+ Buffer result = new Buffer();
+
+ // Writing to result directly will yield packed segments. Instead, write to
+ // other buffers, then write those buffers to result.
+ for (int pos = 0, byteCount; pos < data.length; pos += byteCount) {
+ byteCount = (Segment.SIZE / 2) + dice.nextInt(Segment.SIZE / 2);
+ if (byteCount > data.length - pos) byteCount = data.length - pos;
+ int offset = dice.nextInt(Segment.SIZE - byteCount);
+
+ Buffer segment = new Buffer();
+ segment.write(new byte[offset]);
+ segment.write(data, pos, byteCount);
+ segment.skip(offset);
+
+ result.write(segment, byteCount);
+ }
+
+ return result;
+ }
+}
diff --git a/okio/okio/src/test/java/okio/BufferedSinkTest.java b/okio/okio/src/test/java/okio/BufferedSinkTest.java
new file mode 100644
index 0000000..7afd71d
--- /dev/null
+++ b/okio/okio/src/test/java/okio/BufferedSinkTest.java
@@ -0,0 +1,218 @@
+package okio;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static java.util.Arrays.asList;
+import static okio.TestUtil.repeat;
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public class BufferedSinkTest {
+ private interface Factory {
+ BufferedSink create(Buffer data);
+ }
+
+ // ANDROID-BEGIN
+ // @Parameterized.Parameters(name = "{0}")
+ // ANDROID-END
+ public static List<Object[]> parameters() {
+ return Arrays.asList(new Object[] {
+ new Factory() {
+ @Override public BufferedSink create(Buffer data) {
+ return data;
+ }
+
+ @Override public String toString() {
+ return "Buffer";
+ }
+ }
+ }, new Object[] {
+ new Factory() {
+ @Override public BufferedSink create(Buffer data) {
+ return new RealBufferedSink(data);
+ }
+
+ @Override public String toString() {
+ return "RealBufferedSink";
+ }
+ }
+ });
+ }
+
+ // ANDROID-BEGIN
+ // @Parameterized.Parameter
+ public Factory factory = (Factory) (parameters().get(0))[0];
+ // ANDROID-END
+
+ private Buffer data;
+ private BufferedSink sink;
+
+ @Before public void setUp() {
+ data = new Buffer();
+ sink = factory.create(data);
+ }
+
+ @Test public void writeNothing() throws IOException {
+ sink.writeUtf8("");
+ sink.flush();
+ assertEquals(0, data.size());
+ }
+
+ @Test public void writeBytes() throws Exception {
+ sink.writeByte(0xab);
+ sink.writeByte(0xcd);
+ sink.flush();
+ assertEquals("Buffer[size=2 data=abcd]", data.toString());
+ }
+
+ @Test public void writeLastByteInSegment() throws Exception {
+ sink.writeUtf8(repeat('a', Segment.SIZE - 1));
+ sink.writeByte(0x20);
+ sink.writeByte(0x21);
+ sink.flush();
+ assertEquals(asList(Segment.SIZE, 1), data.segmentSizes());
+ assertEquals(repeat('a', Segment.SIZE - 1), data.readUtf8(Segment.SIZE - 1));
+ assertEquals("Buffer[size=2 data=2021]", data.toString());
+ }
+
+ @Test public void writeShort() throws Exception {
+ sink.writeShort(0xabcd);
+ sink.writeShort(0x4321);
+ sink.flush();
+ assertEquals("Buffer[size=4 data=abcd4321]", data.toString());
+ }
+
+ @Test public void writeShortLe() throws Exception {
+ sink.writeShortLe(0xabcd);
+ sink.writeShortLe(0x4321);
+ sink.flush();
+ assertEquals("Buffer[size=4 data=cdab2143]", data.toString());
+ }
+
+ @Test public void writeInt() throws Exception {
+ sink.writeInt(0xabcdef01);
+ sink.writeInt(0x87654321);
+ sink.flush();
+ assertEquals("Buffer[size=8 data=abcdef0187654321]", data.toString());
+ }
+
+ @Test public void writeLastIntegerInSegment() throws Exception {
+ sink.writeUtf8(repeat('a', Segment.SIZE - 4));
+ sink.writeInt(0xabcdef01);
+ sink.writeInt(0x87654321);
+ sink.flush();
+ assertEquals(asList(Segment.SIZE, 4), data.segmentSizes());
+ assertEquals(repeat('a', Segment.SIZE - 4), data.readUtf8(Segment.SIZE - 4));
+ assertEquals("Buffer[size=8 data=abcdef0187654321]", data.toString());
+ }
+
+ @Test public void writeIntegerDoesNotQuiteFitInSegment() throws Exception {
+ sink.writeUtf8(repeat('a', Segment.SIZE - 3));
+ sink.writeInt(0xabcdef01);
+ sink.writeInt(0x87654321);
+ sink.flush();
+ assertEquals(asList(Segment.SIZE - 3, 8), data.segmentSizes());
+ assertEquals(repeat('a', Segment.SIZE - 3), data.readUtf8(Segment.SIZE - 3));
+ assertEquals("Buffer[size=8 data=abcdef0187654321]", data.toString());
+ }
+
+ @Test public void writeIntLe() throws Exception {
+ sink.writeIntLe(0xabcdef01);
+ sink.writeIntLe(0x87654321);
+ sink.flush();
+ assertEquals("Buffer[size=8 data=01efcdab21436587]", data.toString());
+ }
+
+ @Test public void writeLong() throws Exception {
+ sink.writeLong(0xabcdef0187654321L);
+ sink.writeLong(0xcafebabeb0b15c00L);
+ sink.flush();
+ assertEquals("Buffer[size=16 data=abcdef0187654321cafebabeb0b15c00]", data.toString());
+ }
+
+ @Test public void writeLongLe() throws Exception {
+ sink.writeLongLe(0xabcdef0187654321L);
+ sink.writeLongLe(0xcafebabeb0b15c00L);
+ sink.flush();
+ assertEquals("Buffer[size=16 data=2143658701efcdab005cb1b0bebafeca]", data.toString());
+ }
+
+ @Test public void writeSpecificCharset() throws Exception {
+ sink.writeString("təˈranəˌsôr", Charset.forName("utf-32be"));
+ sink.flush();
+ assertEquals(ByteString.decodeHex("0000007400000259000002c800000072000000610000006e00000259"
+ + "000002cc00000073000000f400000072"), data.readByteString());
+ }
+
+ @Test public void writeAll() throws Exception {
+ Buffer source = new Buffer().writeUtf8("abcdef");
+
+ assertEquals(6, sink.writeAll(source));
+ assertEquals(0, source.size());
+ sink.flush();
+ assertEquals("abcdef", data.readUtf8());
+ }
+
+ @Test public void writeSource() throws Exception {
+ Buffer source = new Buffer().writeUtf8("abcdef");
+
+ // Force resolution of the Source method overload.
+ sink.write((Source) source, 4);
+ sink.flush();
+ assertEquals("abcd", data.readUtf8());
+ assertEquals("ef", source.readUtf8());
+ }
+
+ @Test public void writeSourceWithZeroIsNoOp() throws IOException {
+ // This test ensures that a zero byte count never calls through to read the source. It may be
+ // tied to something like a socket which will potentially block trying to read a segment when
+ // ultimately we don't want any data.
+ Source source = new ForwardingSource(new Buffer()) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ throw new AssertionError();
+ }
+ };
+ sink.write(source, 0);
+ assertEquals(0, data.size());
+ }
+
+ @Test public void writeAllExhausted() throws Exception {
+ Buffer source = new Buffer();
+ assertEquals(0, sink.writeAll(source));
+ assertEquals(0, source.size());
+ }
+
+ @Test public void closeEmitsBufferedBytes() throws IOException {
+ sink.writeByte('a');
+ sink.close();
+ assertEquals('a', data.readByte());
+ }
+
+ @Test public void outputStream() throws Exception {
+ OutputStream out = sink.outputStream();
+ out.write('a');
+ out.write(repeat('b', 9998).getBytes(UTF_8));
+ out.write('c');
+ out.flush();
+ assertEquals("a" + repeat('b', 9998) + "c", data.readUtf8());
+ }
+
+ @Test public void outputStreamBounds() throws Exception {
+ OutputStream out = sink.outputStream();
+ try {
+ out.write(new byte[100], 50, 51);
+ fail();
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ }
+ }
+}
diff --git a/okio/okio/src/test/java/okio/BufferedSourceTest.java b/okio/okio/src/test/java/okio/BufferedSourceTest.java
new file mode 100644
index 0000000..64983f9
--- /dev/null
+++ b/okio/okio/src/test/java/okio/BufferedSourceTest.java
@@ -0,0 +1,475 @@
+package okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.Charset;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static okio.TestUtil.assertByteArrayEquals;
+import static okio.TestUtil.assertByteArraysEquals;
+import static okio.TestUtil.repeat;
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public class BufferedSourceTest {
+ private interface Factory {
+ BufferedSource create(Buffer data);
+ }
+
+ // ANDROID-BEGIN
+ // @Parameterized.Parameters(name = "{0}")
+ // ANDROID-END
+ public static List<Object[]> parameters() {
+ return Arrays.asList(
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return data;
+ }
+
+ @Override public String toString() {
+ return "Buffer";
+ }
+ }},
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return new RealBufferedSource(data);
+ }
+
+ @Override public String toString() {
+ return "RealBufferedSource";
+ }
+ }}
+ );
+ }
+
+ // ANDROID-BEGIN
+ // @Parameterized.Parameter
+ public Factory factory = (Factory) (parameters().get(0))[0];
+ // ANDROID-END
+
+ private Buffer data;
+ private BufferedSource source;
+
+ @Before public void setUp() {
+ data = new Buffer();
+ source = factory.create(data);
+ }
+
+ @Test public void readBytes() throws Exception {
+ data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
+ assertEquals(0xab, source.readByte() & 0xff);
+ assertEquals(0xcd, source.readByte() & 0xff);
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readShort() throws Exception {
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
+ });
+ assertEquals((short) 0xabcd, source.readShort());
+ assertEquals((short) 0xef01, source.readShort());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readShortLe() throws Exception {
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10
+ });
+ assertEquals((short) 0xcdab, source.readShortLe());
+ assertEquals((short) 0x10ef, source.readShortLe());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readShortSplitAcrossMultipleSegments() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE - 1));
+ data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
+ source.skip(Segment.SIZE - 1);
+ assertEquals((short) 0xabcd, source.readShort());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readInt() throws Exception {
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21
+ });
+ assertEquals(0xabcdef01, source.readInt());
+ assertEquals(0x87654321, source.readInt());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readIntLe() throws Exception {
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21
+ });
+ assertEquals(0x10efcdab, source.readIntLe());
+ assertEquals(0x21436587, source.readIntLe());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readIntSplitAcrossMultipleSegments() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE - 3));
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
+ });
+ source.skip(Segment.SIZE - 3);
+ assertEquals(0xabcdef01, source.readInt());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readLong() throws Exception {
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21, (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69, (byte) 0x12, (byte) 0x23,
+ (byte) 0x34, (byte) 0x45
+ });
+ assertEquals(0xabcdef1087654321L, source.readLong());
+ assertEquals(0x3647586912233445L, source.readLong());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readLongLe() throws Exception {
+ data.write(new byte[]{
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21, (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69, (byte) 0x12, (byte) 0x23,
+ (byte) 0x34, (byte) 0x45
+ });
+ assertEquals(0x2143658710efcdabL, source.readLongLe());
+ assertEquals(0x4534231269584736L, source.readLongLe());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readLongSplitAcrossMultipleSegments() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE - 7));
+ data.write(new byte[] {
+ (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01, (byte) 0x87, (byte) 0x65, (byte) 0x43,
+ (byte) 0x21,
+ });
+ source.skip(Segment.SIZE - 7);
+ assertEquals(0xabcdef0187654321L, source.readLong());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readAll() throws IOException {
+ source.buffer().writeUtf8("abc");
+ data.writeUtf8("def");
+
+ Buffer sink = new Buffer();
+ assertEquals(6, source.readAll(sink));
+ assertEquals("abcdef", sink.readUtf8());
+ assertTrue(data.exhausted());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void readAllExhausted() throws IOException {
+ MockSink mockSink = new MockSink();
+ assertEquals(0, source.readAll(mockSink));
+ assertTrue(data.exhausted());
+ assertTrue(source.exhausted());
+ mockSink.assertLog();
+ }
+
+ @Test public void readExhaustedSource() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('a', 10));
+ assertEquals(-1, source.read(sink, 10));
+ assertEquals(10, sink.size());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readZeroBytesFromSource() throws Exception {
+ Buffer sink = new Buffer();
+ sink.writeUtf8(repeat('a', 10));
+
+ // Either 0 or -1 is reasonable here. For consistency with Android's
+ // ByteArrayInputStream we return 0.
+ assertEquals(-1, source.read(sink, 0));
+ assertEquals(10, sink.size());
+ assertEquals(0, data.size());
+ }
+
+ @Test public void readFully() throws Exception {
+ data.writeUtf8(repeat('a', 10000));
+ Buffer sink = new Buffer();
+ source.readFully(sink, 9999);
+ assertEquals(repeat('a', 9999), sink.readUtf8());
+ assertEquals("a", source.readUtf8());
+ }
+
+ @Test public void readFullyTooShortThrows() throws IOException {
+ data.writeUtf8("Hi");
+ Buffer sink = new Buffer();
+ try {
+ source.readFully(sink, 5);
+ fail();
+ } catch (EOFException ignored) {
+ }
+
+ // Verify we read all that we could from the source.
+ assertEquals("Hi", sink.readUtf8());
+ }
+
+ @Test public void readFullyByteArray() throws IOException {
+ data.writeUtf8("Hello").writeUtf8(repeat('e', Segment.SIZE));
+ byte[] expected = data.clone().readByteArray();
+
+ byte[] sink = new byte[Segment.SIZE + 5];
+ source.readFully(sink);
+ assertByteArraysEquals(expected, sink);
+ }
+
+ @Test public void readFullyByteArrayTooShortThrows() throws IOException {
+ data.writeUtf8("Hello");
+
+ byte[] sink = new byte[6];
+ try {
+ source.readFully(sink);
+ fail();
+ } catch (EOFException ignored) {
+ }
+
+ // Verify we read all that we could from the source.
+ assertByteArraysEquals(new byte[]{'H', 'e', 'l', 'l', 'o', 0}, sink);
+ }
+
+ @Test public void readIntoByteArray() throws IOException {
+ data.writeUtf8("abcd");
+
+ byte[] sink = new byte[3];
+ int read = source.read(sink);
+ assertEquals(3, read);
+ byte[] expected = { 'a', 'b', 'c' };
+ assertByteArraysEquals(expected, sink);
+ }
+
+ @Test public void readIntoByteArrayNotEnough() throws IOException {
+ data.writeUtf8("abcd");
+
+ byte[] sink = new byte[5];
+ int read = source.read(sink);
+ assertEquals(4, read);
+ byte[] expected = { 'a', 'b', 'c', 'd', 0 };
+ assertByteArraysEquals(expected, sink);
+ }
+
+ @Test public void readIntoByteArrayOffsetAndCount() throws IOException {
+ data.writeUtf8("abcd");
+
+ byte[] sink = new byte[7];
+ int read = source.read(sink, 2, 3);
+ assertEquals(3, read);
+ byte[] expected = { 0, 0, 'a', 'b', 'c', 0, 0 };
+ assertByteArraysEquals(expected, sink);
+ }
+
+ @Test public void readByteArray() throws IOException {
+ String string = "abcd" + repeat('e', Segment.SIZE);
+ data.writeUtf8(string);
+ assertByteArraysEquals(string.getBytes(UTF_8), source.readByteArray());
+ }
+
+ @Test public void readByteArrayPartial() throws IOException {
+ data.writeUtf8("abcd");
+ assertEquals("[97, 98, 99]", Arrays.toString(source.readByteArray(3)));
+ assertEquals("d", source.readUtf8(1));
+ }
+
+ @Test public void readByteString() throws IOException {
+ data.writeUtf8("abcd").writeUtf8(repeat('e', Segment.SIZE));
+ assertEquals("abcd" + repeat('e', Segment.SIZE), source.readByteString().utf8());
+ }
+
+ @Test public void readByteStringPartial() throws IOException {
+ data.writeUtf8("abcd").writeUtf8(repeat('e', Segment.SIZE));
+ assertEquals("abc", source.readByteString(3).utf8());
+ assertEquals("d", source.readUtf8(1));
+ }
+
+ @Test public void readSpecificCharsetPartial() throws Exception {
+ data.write(ByteString.decodeHex("0000007600000259000002c80000006c000000e40000007300000259"
+ + "000002cc000000720000006100000070000000740000025900000072"));
+ assertEquals("vəˈläsÉ™", source.readString(7 * 4, Charset.forName("utf-32")));
+ }
+
+ @Test public void readSpecificCharset() throws Exception {
+ data.write(ByteString.decodeHex("0000007600000259000002c80000006c000000e40000007300000259"
+ + "000002cc000000720000006100000070000000740000025900000072"));
+ assertEquals("vəˈläsəˌraptÉ™r", source.readString(Charset.forName("utf-32")));
+ }
+
+ @Test public void readUtf8SpansSegments() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE * 2));
+ source.skip(Segment.SIZE - 1);
+ assertEquals("aa", source.readUtf8(2));
+ }
+
+ @Test public void readUtf8Segment() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE));
+ assertEquals(repeat('a', Segment.SIZE), source.readUtf8(Segment.SIZE));
+ }
+
+ @Test public void readUtf8PartialBuffer() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE + 20));
+ assertEquals(repeat('a', Segment.SIZE + 10), source.readUtf8(Segment.SIZE + 10));
+ }
+
+ @Test public void readUtf8EntireBuffer() throws Exception {
+ data.writeUtf8(repeat('a', Segment.SIZE * 2));
+ assertEquals(repeat('a', Segment.SIZE * 2), source.readUtf8());
+ }
+
+ @Test public void skip() throws Exception {
+ data.writeUtf8("a");
+ data.writeUtf8(repeat('b', Segment.SIZE));
+ data.writeUtf8("c");
+ source.skip(1);
+ assertEquals('b', source.readByte() & 0xff);
+ source.skip(Segment.SIZE - 2);
+ assertEquals('b', source.readByte() & 0xff);
+ source.skip(1);
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void skipInsufficientData() throws Exception {
+ data.writeUtf8("a");
+
+ try {
+ source.skip(2);
+ fail();
+ } catch (EOFException ignored) {
+ }
+ }
+
+ @Test public void indexOf() throws Exception {
+ // The segment is empty.
+ assertEquals(-1, source.indexOf((byte) 'a'));
+
+ // The segment has one value.
+ data.writeUtf8("a"); // a
+ assertEquals(0, source.indexOf((byte) 'a'));
+ assertEquals(-1, source.indexOf((byte) 'b'));
+
+ // The segment has lots of data.
+ data.writeUtf8(repeat('b', Segment.SIZE - 2)); // ab...b
+ assertEquals(0, source.indexOf((byte) 'a'));
+ assertEquals(1, source.indexOf((byte) 'b'));
+ assertEquals(-1, source.indexOf((byte) 'c'));
+
+ // The segment doesn't start at 0, it starts at 2.
+ source.skip(2); // b...b
+ assertEquals(-1, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'b'));
+ assertEquals(-1, source.indexOf((byte) 'c'));
+
+ // The segment is full.
+ data.writeUtf8("c"); // b...bc
+ assertEquals(-1, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'b'));
+ assertEquals(Segment.SIZE - 3, source.indexOf((byte) 'c'));
+
+ // The segment doesn't start at 2, it starts at 4.
+ source.skip(2); // b...bc
+ assertEquals(-1, source.indexOf((byte) 'a'));
+ assertEquals(0, source.indexOf((byte) 'b'));
+ assertEquals(Segment.SIZE - 5, source.indexOf((byte) 'c'));
+
+ // Two segments.
+ data.writeUtf8("d"); // b...bcd, d is in the 2nd segment.
+ assertEquals(Segment.SIZE - 4, source.indexOf((byte) 'd'));
+ assertEquals(-1, source.indexOf((byte) 'e'));
+ }
+
+ @Test public void indexOfWithOffset() throws IOException {
+ data.writeUtf8("a").writeUtf8(repeat('b', Segment.SIZE)).writeUtf8("c");
+ assertEquals(-1, source.indexOf((byte) 'a', 1));
+ assertEquals(15, source.indexOf((byte) 'b', 15));
+ }
+
+ @Test public void indexOfElement() throws IOException {
+ data.writeUtf8("a").writeUtf8(repeat('b', Segment.SIZE)).writeUtf8("c");
+ assertEquals(0, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK")));
+ assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJKb")));
+ assertEquals(Segment.SIZE + 1, source.indexOfElement(ByteString.encodeUtf8("cDEFGHIJK")));
+ assertEquals(1, source.indexOfElement(ByteString.encodeUtf8("DEFbGHIc")));
+ assertEquals(-1L, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJK")));
+ assertEquals(-1L, source.indexOfElement(ByteString.encodeUtf8("")));
+ }
+
+ @Test public void indexOfElementWithOffset() throws IOException {
+ data.writeUtf8("a").writeUtf8(repeat('b', Segment.SIZE)).writeUtf8("c");
+ assertEquals(-1, source.indexOfElement(ByteString.encodeUtf8("DEFGaHIJK"), 1));
+ assertEquals(15, source.indexOfElement(ByteString.encodeUtf8("DEFGHIJKb"), 15));
+ }
+
+ @Test public void request() throws IOException {
+ data.writeUtf8("a").writeUtf8(repeat('b', Segment.SIZE)).writeUtf8("c");
+ assertTrue(source.request(Segment.SIZE + 2));
+ assertFalse(source.request(Segment.SIZE + 3));
+ }
+
+ @Test public void require() throws IOException {
+ data.writeUtf8("a").writeUtf8(repeat('b', Segment.SIZE)).writeUtf8("c");
+ source.require(Segment.SIZE + 2);
+ try {
+ source.require(Segment.SIZE + 3);
+ fail();
+ } catch (EOFException expected) {
+ }
+ }
+
+ @Test public void inputStream() throws Exception {
+ data.writeUtf8("abc");
+ InputStream in = source.inputStream();
+ byte[] bytes = new byte[3];
+ int read = in.read(bytes);
+ assertEquals(3, read);
+ assertByteArrayEquals("abc", bytes);
+ assertEquals(-1, in.read());
+ }
+
+ @Test public void inputStreamOffsetCount() throws Exception {
+ data.writeUtf8("abcde");
+ InputStream in = source.inputStream();
+ byte[] bytes = { 'z', 'z', 'z', 'z', 'z' };
+ int read = in.read(bytes, 1, 3);
+ assertEquals(3, read);
+ assertByteArrayEquals("zabcz", bytes);
+ }
+
+ @Test public void inputStreamSkip() throws Exception {
+ data.writeUtf8("abcde");
+ InputStream in = source.inputStream();
+ assertEquals(4, in.skip(4));
+ assertEquals('e', in.read());
+ }
+
+ @Test public void inputStreamCharByChar() throws Exception {
+ data.writeUtf8("abc");
+ InputStream in = source.inputStream();
+ assertEquals('a', in.read());
+ assertEquals('b', in.read());
+ assertEquals('c', in.read());
+ assertEquals(-1, in.read());
+ }
+
+ @Test public void inputStreamBounds() throws IOException {
+ data.writeUtf8(repeat('a', 100));
+ InputStream in = source.inputStream();
+ try {
+ in.read(new byte[100], 50, 51);
+ fail();
+ } catch (ArrayIndexOutOfBoundsException expected) {
+ }
+ }
+}
diff --git a/okio/src/test/java/okio/ByteStringTest.java b/okio/okio/src/test/java/okio/ByteStringTest.java
similarity index 71%
rename from okio/src/test/java/okio/ByteStringTest.java
rename to okio/okio/src/test/java/okio/ByteStringTest.java
index 16b8e2d..528ed05 100644
--- a/okio/src/test/java/okio/ByteStringTest.java
+++ b/okio/okio/src/test/java/okio/ByteStringTest.java
@@ -18,17 +18,28 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
-import java.util.Arrays;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
import org.junit.Test;
+import static okio.TestUtil.assertByteArraysEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class ByteStringTest {
+ @Test public void ofCopyRange() {
+ byte[] bytes = "Hello, World!".getBytes(Util.UTF_8);
+ ByteString byteString = ByteString.of(bytes, 2, 9);
+ // Verify that the bytes were copied out.
+ bytes[4] = (byte) 'a';
+ assertEquals("llo, Worl", byteString.utf8());
+ }
+
@Test public void getByte() throws Exception {
ByteString byteString = ByteString.decodeHex("ab12");
assertEquals(-85, byteString.getByte(0));
@@ -76,7 +87,7 @@
assertEquals(ByteString.of(), ByteString.read(in, 0));
}
- @Test public void readLowerCase() throws Exception {
+ @Test public void readAndToLowercase() throws Exception {
InputStream in = new ByteArrayInputStream("ABC".getBytes(Util.UTF_8));
assertEquals(ByteString.encodeUtf8("ab"), ByteString.read(in, 2).toAsciiLowercase());
assertEquals(ByteString.encodeUtf8("c"), ByteString.read(in, 1).toAsciiLowercase());
@@ -96,6 +107,17 @@
assertEquals(ByteString.encodeUtf8("abcd"), ByteString.encodeUtf8("abCD").toAsciiLowercase());
}
+ @Test public void readAndToUppercase() throws Exception {
+ InputStream in = new ByteArrayInputStream("abc".getBytes(Util.UTF_8));
+ assertEquals(ByteString.encodeUtf8("AB"), ByteString.read(in, 2).toAsciiUppercase());
+ assertEquals(ByteString.encodeUtf8("C"), ByteString.read(in, 1).toAsciiUppercase());
+ assertEquals(ByteString.EMPTY, ByteString.read(in, 0).toAsciiUppercase());
+ }
+
+ @Test public void toAsciiStartsUppercaseEndsLowercase() throws Exception {
+ assertEquals(ByteString.encodeUtf8("ABCD"), ByteString.encodeUtf8("ABcd").toAsciiUppercase());
+ }
+
@Test public void write() throws Exception {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ByteString.decodeHex("616263").write(out);
@@ -176,7 +198,41 @@
ByteString.encodeUtf8("12345678901234567").toString());
}
- private static void assertByteArraysEquals(byte[] a, byte[] b) {
- assertEquals(Arrays.toString(a), Arrays.toString(b));
+ @Test public void javaSerializationTestNonEmpty() throws Exception {
+ ByteString original = ByteString.encodeUtf8(bronzeHorseman);
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bytes);
+ out.writeObject("before");
+ out.writeObject(original);
+ out.writeObject("after");
+ ObjectInputStream in = new ObjectInputStream(
+ new ByteArrayInputStream(bytes.toByteArray()));
+ assertEquals("before", in.readObject());
+ Object roundTrippedObject = in.readObject();
+ assertNotNull(roundTrippedObject);
+ assertTrue("Round tripped object wasn't a ByteString but a " +
+ roundTrippedObject.getClass(), roundTrippedObject instanceof ByteString);
+ assertEquals(original, roundTrippedObject);
+ assertEquals("hashCodes", original.hashCode(), roundTrippedObject.hashCode());
+ assertEquals("after", in.readObject());
+ }
+
+ @Test public void javaSerializationTestEmpty() throws Exception {
+ ByteString original = ByteString.of();
+ ByteArrayOutputStream bytes = new ByteArrayOutputStream();
+ ObjectOutputStream out = new ObjectOutputStream(bytes);
+ out.writeObject("before");
+ out.writeObject(original);
+ out.writeObject("after");
+ ObjectInputStream in = new ObjectInputStream(
+ new ByteArrayInputStream(bytes.toByteArray()));
+ assertEquals("before", in.readObject());
+ Object roundTrippedObject = in.readObject();
+ assertNotNull(roundTrippedObject);
+ assertTrue("Round tripped object wasn't a ByteString but a " +
+ roundTrippedObject.getClass(), roundTrippedObject instanceof ByteString);
+ assertEquals(original, roundTrippedObject);
+ assertEquals("hashCodes", original.hashCode(), roundTrippedObject.hashCode());
+ assertEquals("after", in.readObject());
}
}
diff --git a/okio/src/test/java/okio/DeflaterSinkTest.java b/okio/okio/src/test/java/okio/DeflaterSinkTest.java
similarity index 73%
rename from okio/src/test/java/okio/DeflaterSinkTest.java
rename to okio/okio/src/test/java/okio/DeflaterSinkTest.java
index 0f6b8c2..87d6c59 100644
--- a/okio/src/test/java/okio/DeflaterSinkTest.java
+++ b/okio/okio/src/test/java/okio/DeflaterSinkTest.java
@@ -17,72 +17,72 @@
import java.io.IOException;
import java.io.InputStream;
-import java.util.Arrays;
-import java.util.Random;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import java.util.zip.InflaterInputStream;
import org.junit.Test;
+import static okio.TestUtil.randomBytes;
+import static okio.TestUtil.repeat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public final class DeflaterSinkTest {
@Test public void deflateWithClose() throws Exception {
- OkBuffer data = new OkBuffer();
+ Buffer data = new Buffer();
String original = "They're moving in herds. They do move in herds.";
data.writeUtf8(original);
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
deflaterSink.write(data, data.size());
deflaterSink.close();
- OkBuffer inflated = inflate(sink);
- assertEquals(original, inflated.readUtf8(inflated.size()));
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
}
@Test public void deflateWithSyncFlush() throws Exception {
String original = "Yes, yes, yes. That's why we're taking extreme precautions.";
- OkBuffer data = new OkBuffer();
+ Buffer data = new Buffer();
data.writeUtf8(original);
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
deflaterSink.write(data, data.size());
deflaterSink.flush();
- OkBuffer inflated = inflate(sink);
- assertEquals(original, inflated.readUtf8(inflated.size()));
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
}
@Test public void deflateWellCompressed() throws IOException {
String original = repeat('a', 1024 * 1024);
- OkBuffer data = new OkBuffer();
+ Buffer data = new Buffer();
data.writeUtf8(original);
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
deflaterSink.write(data, data.size());
deflaterSink.close();
- OkBuffer inflated = inflate(sink);
- assertEquals(original, inflated.readUtf8(inflated.size()));
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readUtf8());
}
@Test public void deflatePoorlyCompressed() throws IOException {
ByteString original = randomBytes(1024 * 1024);
- OkBuffer data = new OkBuffer();
+ Buffer data = new Buffer();
data.write(original);
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
DeflaterSink deflaterSink = new DeflaterSink(sink, new Deflater());
deflaterSink.write(data, data.size());
deflaterSink.close();
- OkBuffer inflated = inflate(sink);
- assertEquals(original, inflated.readByteString(inflated.size()));
+ Buffer inflated = inflate(sink);
+ assertEquals(original, inflated.readByteString());
}
@Test public void multipleSegmentsWithoutCompression() throws IOException {
- OkBuffer buffer = new OkBuffer();
+ Buffer buffer = new Buffer();
Deflater deflater = new Deflater();
deflater.setLevel(Deflater.NO_COMPRESSION);
DeflaterSink deflaterSink = new DeflaterSink(buffer, deflater);
int byteCount = Segment.SIZE * 4;
- deflaterSink.write(new OkBuffer().writeUtf8(repeat('a', byteCount)), byteCount);
+ deflaterSink.write(new Buffer().writeUtf8(repeat('a', byteCount)), byteCount);
deflaterSink.close();
assertEquals(repeat('a', byteCount), inflate(buffer).readUtf8(byteCount));
}
@@ -99,7 +99,7 @@
Deflater deflater = new Deflater();
deflater.setLevel(Deflater.NO_COMPRESSION);
DeflaterSink deflaterSink = new DeflaterSink(mockSink, deflater);
- deflaterSink.write(new OkBuffer().writeUtf8(repeat('a', Segment.SIZE)), Segment.SIZE);
+ deflaterSink.write(new Buffer().writeUtf8(repeat('a', Segment.SIZE)), Segment.SIZE);
try {
deflaterSink.close();
fail();
@@ -113,11 +113,11 @@
* Uses streaming decompression to inflate {@code deflated}. The input must
* either be finished or have a trailing sync flush.
*/
- private OkBuffer inflate(OkBuffer deflated) throws IOException {
+ private Buffer inflate(Buffer deflated) throws IOException {
InputStream deflatedIn = deflated.inputStream();
Inflater inflater = new Inflater();
InputStream inflatedIn = new InflaterInputStream(deflatedIn, inflater);
- OkBuffer result = new OkBuffer();
+ Buffer result = new Buffer();
byte[] buffer = new byte[8192];
while (!inflater.needsInput() || deflated.size() > 0 || deflatedIn.available() > 0) {
int count = inflatedIn.read(buffer, 0, buffer.length);
@@ -125,17 +125,4 @@
}
return result;
}
-
- private ByteString randomBytes(int length) {
- Random random = new Random(0);
- byte[] randomBytes = new byte[length];
- random.nextBytes(randomBytes);
- return ByteString.of(randomBytes);
- }
-
- private String repeat(char c, int count) {
- char[] array = new char[count];
- Arrays.fill(array, c);
- return new String(array);
- }
}
diff --git a/okio/okio/src/test/java/okio/GzipSinkTest.java b/okio/okio/src/test/java/okio/GzipSinkTest.java
new file mode 100644
index 0000000..c4d1117
--- /dev/null
+++ b/okio/okio/src/test/java/okio/GzipSinkTest.java
@@ -0,0 +1,45 @@
+package okio;
+
+import java.io.IOException;
+import org.junit.Test;
+
+import static okio.TestUtil.repeat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public class GzipSinkTest {
+ @Test public void gzipGunzip() throws Exception {
+ Buffer data = new Buffer();
+ String original = "It's a UNIX system! I know this!";
+ data.writeUtf8(original);
+ Buffer sink = new Buffer();
+ GzipSink gzipSink = new GzipSink(sink);
+ gzipSink.write(data, data.size());
+ gzipSink.close();
+ Buffer inflated = gunzip(sink);
+ assertEquals(original, inflated.readUtf8());
+ }
+
+ @Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
+ MockSink mockSink = new MockSink();
+ mockSink.scheduleThrow(0, new IOException("first"));
+ mockSink.scheduleThrow(1, new IOException("second"));
+ GzipSink gzipSink = new GzipSink(mockSink);
+ gzipSink.write(new Buffer().writeUtf8(repeat('a', Segment.SIZE)), Segment.SIZE);
+ try {
+ gzipSink.close();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("first", expected.getMessage());
+ }
+ mockSink.assertLogContains("close()");
+ }
+
+ private Buffer gunzip(Buffer gzipped) throws IOException {
+ Buffer result = new Buffer();
+ GzipSource source = new GzipSource(gzipped);
+ while (source.read(result, Integer.MAX_VALUE) != -1) {
+ }
+ return result;
+ }
+}
diff --git a/okio/src/test/java/okio/GzipSourceTest.java b/okio/okio/src/test/java/okio/GzipSourceTest.java
similarity index 87%
rename from okio/src/test/java/okio/GzipSourceTest.java
rename to okio/okio/src/test/java/okio/GzipSourceTest.java
index f14b999..bd3c329 100644
--- a/okio/src/test/java/okio/GzipSourceTest.java
+++ b/okio/okio/src/test/java/okio/GzipSourceTest.java
@@ -28,7 +28,7 @@
public class GzipSourceTest {
@Test public void gunzip() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeader);
gzipped.write(deflated);
gzipped.write(gzipTrailer);
@@ -40,7 +40,7 @@
ByteString gzipHeader = gzipHeaderWithFlags((byte) 0x02);
hcrc.update(gzipHeader.toByteArray());
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeader);
gzipped.writeShort(Util.reverseBytesShort((short) hcrc.getValue())); // little endian
gzipped.write(deflated);
@@ -49,7 +49,7 @@
}
@Test public void gunzip_withExtra() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeaderWithFlags((byte) 0x04));
gzipped.writeShort(Util.reverseBytesShort((short) 7)); // little endian extra length
gzipped.write("blubber".getBytes(UTF_8), 0, 7);
@@ -59,7 +59,7 @@
}
@Test public void gunzip_withName() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeaderWithFlags((byte) 0x08));
gzipped.write("foo.txt".getBytes(UTF_8), 0, 7);
gzipped.writeByte(0); // zero-terminated
@@ -69,7 +69,7 @@
}
@Test public void gunzip_withComment() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeaderWithFlags((byte) 0x10));
gzipped.write("rubbish".getBytes(UTF_8), 0, 7);
gzipped.writeByte(0); // zero-terminated
@@ -83,7 +83,7 @@
* {@code echo gzipped | base64 --decode | gzip -l -v}
*/
@Test public void gunzip_withAll() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeaderWithFlags((byte) 0x1c));
gzipped.writeShort(Util.reverseBytesShort((short) 7)); // little endian extra length
gzipped.write("blubber".getBytes(UTF_8), 0, 7);
@@ -96,9 +96,9 @@
assertGzipped(gzipped);
}
- private void assertGzipped(OkBuffer gzipped) throws IOException {
- OkBuffer gunzipped = gunzip(gzipped);
- assertEquals("It's a UNIX system! I know this!", gunzipped.readUtf8(gunzipped.size()));
+ private void assertGzipped(Buffer gzipped) throws IOException {
+ Buffer gunzipped = gunzip(gzipped);
+ assertEquals("It's a UNIX system! I know this!", gunzipped.readUtf8());
}
/**
@@ -106,7 +106,7 @@
* CONTINUATION, not HCRC. For example, this is the case with the default gzip on osx.
*/
@Test public void gunzipWhenHeaderCRCIncorrect() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeaderWithFlags((byte) 0x02));
gzipped.writeShort((short) 0); // wrong HCRC!
gzipped.write(deflated);
@@ -121,7 +121,7 @@
}
@Test public void gunzipWhenCRCIncorrect() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeader);
gzipped.write(deflated);
gzipped.writeInt(Util.reverseBytesInt(0x1234567)); // wrong CRC
@@ -136,7 +136,7 @@
}
@Test public void gunzipWhenLengthIncorrect() throws Exception {
- OkBuffer gzipped = new OkBuffer();
+ Buffer gzipped = new Buffer();
gzipped.write(gzipHeader);
gzipped.write(deflated);
gzipped.write(gzipTrailer.toByteArray(), 0, 4);
@@ -151,7 +151,7 @@
}
@Test public void gunzipExhaustsSource() throws Exception {
- OkBuffer gzippedSource = new OkBuffer()
+ Buffer gzippedSource = new Buffer()
.write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc'
ExhaustableSource exhaustableSource = new ExhaustableSource(gzippedSource);
@@ -161,12 +161,12 @@
assertEquals('b', gunzippedSource.readByte());
assertEquals('c', gunzippedSource.readByte());
assertFalse(exhaustableSource.exhausted);
- assertEquals(-1, gunzippedSource.read(new OkBuffer(), 1));
+ assertEquals(-1, gunzippedSource.read(new Buffer(), 1));
assertTrue(exhaustableSource.exhausted);
}
@Test public void gunzipThrowsIfSourceIsNotExhausted() throws Exception {
- OkBuffer gzippedSource = new OkBuffer()
+ Buffer gzippedSource = new Buffer()
.write(ByteString.decodeHex("1f8b08000000000000004b4c4a0600c241243503000000")); // 'abc'
gzippedSource.writeByte('d'); // This byte shouldn't be here!
@@ -199,15 +199,15 @@
+ "20000000" // 32 in little endian.
);
- private OkBuffer gunzip(OkBuffer gzipped) throws IOException {
- OkBuffer result = new OkBuffer();
+ private Buffer gunzip(Buffer gzipped) throws IOException {
+ Buffer result = new Buffer();
GzipSource source = new GzipSource(gzipped);
while (source.read(result, Integer.MAX_VALUE) != -1) {
}
return result;
}
- /** This source keeps track of whether its read have returned -1. */
+ /** This source keeps track of whether its read has returned -1. */
static class ExhaustableSource implements Source {
private final Source source;
private boolean exhausted;
@@ -216,15 +216,14 @@
this.source = source;
}
- @Override public long read(OkBuffer sink, long byteCount) throws IOException {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
long result = source.read(sink, byteCount);
if (result == -1) exhausted = true;
return result;
}
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
+ @Override public Timeout timeout() {
+ return source.timeout();
}
@Override public void close() throws IOException {
diff --git a/okio/src/test/java/okio/InflaterSourceTest.java b/okio/okio/src/test/java/okio/InflaterSourceTest.java
similarity index 69%
rename from okio/src/test/java/okio/InflaterSourceTest.java
rename to okio/okio/src/test/java/okio/InflaterSourceTest.java
index e6f2bc6..e32f18f 100644
--- a/okio/src/test/java/okio/InflaterSourceTest.java
+++ b/okio/okio/src/test/java/okio/InflaterSourceTest.java
@@ -17,25 +17,25 @@
import java.io.EOFException;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.Random;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.Inflater;
import org.junit.Test;
+import static okio.TestUtil.randomBytes;
+import static okio.TestUtil.repeat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public final class InflaterSourceTest {
@Test public void inflate() throws Exception {
- OkBuffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK"
+ Buffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK"
+ "tYDAF6CD5s=");
- OkBuffer inflated = inflate(deflated);
- assertEquals("God help us, we're in the hands of engineers.", readUtf8(inflated));
+ Buffer inflated = inflate(deflated);
+ assertEquals("God help us, we're in the hands of engineers.", inflated.readUtf8());
}
@Test public void inflateTruncated() throws Exception {
- OkBuffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK"
+ Buffer deflated = decodeBase64("eJxzz09RyEjNKVAoLdZRKE9VL0pVyMxTKMlIVchIzEspVshPU0jNS8/MS00tK"
+ "tYDAF6CDw==");
try {
inflate(deflated);
@@ -45,7 +45,7 @@
}
@Test public void inflateWellCompressed() throws Exception {
- OkBuffer deflated = decodeBase64("eJztwTEBAAAAwqCs61/CEL5AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ Buffer deflated = decodeBase64("eJztwTEBAAAAwqCs61/CEL5AAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
@@ -63,53 +63,36 @@
+ "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB8B"
+ "tFeWvE=\n");
String original = repeat('a', 1024 * 1024);
- OkBuffer inflated = inflate(deflated);
- assertEquals(original, readUtf8(inflated));
+ Buffer inflated = inflate(deflated);
+ assertEquals(original, inflated.readUtf8());
}
@Test public void inflatePoorlyCompressed() throws Exception {
ByteString original = randomBytes(1024 * 1024);
- OkBuffer deflated = deflate(original);
- OkBuffer inflated = inflate(deflated);
- assertEquals(original, inflated.readByteString(inflated.size()));
+ Buffer deflated = deflate(original);
+ Buffer inflated = inflate(deflated);
+ assertEquals(original, inflated.readByteString());
}
- private OkBuffer decodeBase64(String s) {
- return new OkBuffer().write(ByteString.decodeBase64(s));
- }
-
- private String readUtf8(OkBuffer buffer) {
- return buffer.readUtf8(buffer.size());
+ private Buffer decodeBase64(String s) {
+ return new Buffer().write(ByteString.decodeBase64(s));
}
/** Use DeflaterOutputStream to deflate source. */
- private OkBuffer deflate(ByteString source) throws IOException {
- OkBuffer result = new OkBuffer();
+ private Buffer deflate(ByteString source) throws IOException {
+ Buffer result = new Buffer();
Sink sink = Okio.sink(new DeflaterOutputStream(result.outputStream()));
- sink.write(new OkBuffer().write(source), source.size());
+ sink.write(new Buffer().write(source), source.size());
sink.close();
return result;
}
/** Returns a new buffer containing the inflated contents of {@code deflated}. */
- private OkBuffer inflate(OkBuffer deflated) throws IOException {
- OkBuffer result = new OkBuffer();
+ private Buffer inflate(Buffer deflated) throws IOException {
+ Buffer result = new Buffer();
InflaterSource source = new InflaterSource(deflated, new Inflater());
while (source.read(result, Integer.MAX_VALUE) != -1) {
}
return result;
}
-
- private ByteString randomBytes(int length) {
- Random random = new Random(0);
- byte[] randomBytes = new byte[length];
- random.nextBytes(randomBytes);
- return ByteString.of(randomBytes);
- }
-
- private String repeat(char c, int count) {
- char[] array = new char[count];
- Arrays.fill(array, c);
- return new String(array);
- }
}
diff --git a/okio/src/test/java/okio/MockSink.java b/okio/okio/src/test/java/okio/MockSink.java
similarity index 91%
rename from okio/src/test/java/okio/MockSink.java
rename to okio/okio/src/test/java/okio/MockSink.java
index bae3259..04cbf2b 100644
--- a/okio/src/test/java/okio/MockSink.java
+++ b/okio/okio/src/test/java/okio/MockSink.java
@@ -47,7 +47,7 @@
if (exception != null) throw exception;
}
- @Override public void write(OkBuffer source, long byteCount) throws IOException {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
log.add("write(" + source + ", " + byteCount + ")");
source.skip(byteCount);
throwIfScheduled();
@@ -58,9 +58,9 @@
throwIfScheduled();
}
- @Override public Sink deadline(Deadline deadline) {
- log.add("deadline()");
- return this;
+ @Override public Timeout timeout() {
+ log.add("timeout()");
+ return Timeout.NONE;
}
@Override public void close() throws IOException {
diff --git a/okio/okio/src/test/java/okio/OkioTest.java b/okio/okio/src/test/java/okio/OkioTest.java
new file mode 100644
index 0000000..815a51f
--- /dev/null
+++ b/okio/okio/src/test/java/okio/OkioTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static okio.TestUtil.repeat;
+import static okio.Util.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class OkioTest {
+ @Rule public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test public void readWriteFile() throws Exception {
+ File file = temporaryFolder.newFile();
+
+ BufferedSink sink = Okio.buffer(Okio.sink(file));
+ sink.writeUtf8("Hello, java.io file!");
+ sink.close();
+ assertTrue(file.exists());
+ assertEquals(20, file.length());
+
+ BufferedSource source = Okio.buffer(Okio.source(file));
+ assertEquals("Hello, java.io file!", source.readUtf8());
+ source.close();
+ }
+
+ @Test public void appendFile() throws Exception {
+ File file = temporaryFolder.newFile();
+
+ BufferedSink sink = Okio.buffer(Okio.appendingSink(file));
+ sink.writeUtf8("Hello, ");
+ sink.close();
+ assertTrue(file.exists());
+ assertEquals(7, file.length());
+
+ sink = Okio.buffer(Okio.appendingSink(file));
+ sink.writeUtf8("java.io file!");
+ sink.close();
+ assertEquals(20, file.length());
+
+ BufferedSource source = Okio.buffer(Okio.source(file));
+ assertEquals("Hello, java.io file!", source.readUtf8());
+ source.close();
+ }
+
+ // ANDROID-BEGIN
+ // @Test public void readWritePath() throws Exception {
+ // Path path = temporaryFolder.newFile().toPath();
+ //
+ // BufferedSink sink = Okio.buffer(Okio.sink(path));
+ // sink.writeUtf8("Hello, java.nio file!");
+ // sink.close();
+ // assertTrue(Files.exists(path));
+ // assertEquals(21, Files.size(path));
+ //
+ // BufferedSource source = Okio.buffer(Okio.source(path));
+ // assertEquals("Hello, java.nio file!", source.readUtf8());
+ // source.close();
+ // }
+ // ANDROID-END
+
+ @Test public void sinkFromOutputStream() throws Exception {
+ Buffer data = new Buffer();
+ data.writeUtf8("a");
+ data.writeUtf8(repeat('b', 9998));
+ data.writeUtf8("c");
+
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ Sink sink = Okio.sink(out);
+ sink.write(data, 3);
+ assertEquals("abb", out.toString("UTF-8"));
+ sink.write(data, data.size());
+ assertEquals("a" + repeat('b', 9998) + "c", out.toString("UTF-8"));
+ }
+
+ @Test public void sourceFromInputStream() throws Exception {
+ InputStream in = new ByteArrayInputStream(
+ ("a" + repeat('b', Segment.SIZE * 2) + "c").getBytes(UTF_8));
+
+ // Source: ab...bc
+ Source source = Okio.source(in);
+ Buffer sink = new Buffer();
+
+ // Source: b...bc. Sink: abb.
+ assertEquals(3, source.read(sink, 3));
+ assertEquals("abb", sink.readUtf8(3));
+
+ // Source: b...bc. Sink: b...b.
+ assertEquals(Segment.SIZE, source.read(sink, 20000));
+ assertEquals(repeat('b', Segment.SIZE), sink.readUtf8());
+
+ // Source: b...bc. Sink: b...bc.
+ assertEquals(Segment.SIZE - 1, source.read(sink, 20000));
+ assertEquals(repeat('b', Segment.SIZE - 2) + "c", sink.readUtf8());
+
+ // Source and sink are empty.
+ assertEquals(-1, source.read(sink, 1));
+ }
+
+ @Test public void sourceFromInputStreamBounds() throws Exception {
+ Source source = Okio.source(new ByteArrayInputStream(new byte[100]));
+ try {
+ source.read(new Buffer(), -1);
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
+ }
+}
diff --git a/okio/okio/src/test/java/okio/ReadUtf8LineTest.java b/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
new file mode 100644
index 0000000..9867ca0
--- /dev/null
+++ b/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+@RunWith(Parameterized.class)
+public final class ReadUtf8LineTest {
+ private interface Factory {
+ BufferedSource create(Buffer data);
+ }
+
+ // ANDROID-BEGIN
+ // @Parameterized.Parameters(name = "{0}")
+ // ANDROID-END
+ public static List<Object[]> parameters() {
+ return Arrays.asList(
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return data;
+ }
+
+ @Override public String toString() {
+ return "Buffer";
+ }
+ }},
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return new RealBufferedSource(data);
+ }
+
+ @Override public String toString() {
+ return "RealBufferedSource";
+ }
+ }},
+ new Object[] { new Factory() {
+ @Override public BufferedSource create(Buffer data) {
+ return new RealBufferedSource(new ForwardingSource(data) {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ return super.read(sink, Math.min(1, byteCount));
+ }
+ });
+ }
+
+ @Override public String toString() {
+ return "Slow RealBufferedSource";
+ }
+ }}
+ );
+ }
+
+ // ANDROID-BEGIN
+ // @Parameterized.Parameter
+ public Factory factory = (Factory) (parameters().get(0))[0];
+ // ANDROID-END
+
+ private Buffer data;
+ private BufferedSource source;
+
+ @Before public void setUp() {
+ data = new Buffer();
+ source = factory.create(data);
+ }
+
+ @Test public void readLines() throws IOException {
+ data.writeUtf8("abc\ndef\n");
+ assertEquals("abc", source.readUtf8LineStrict());
+ assertEquals("def", source.readUtf8LineStrict());
+ try {
+ source.readUtf8LineStrict();
+ fail();
+ } catch (EOFException expected) {
+ assertEquals("\\n not found: size=0 content=...", expected.getMessage());
+ }
+ }
+
+ @Test public void eofExceptionProvidesLimitedContent() throws IOException {
+ data.writeUtf8("aaaaaaaabbbbbbbbccccccccdddddddde");
+ try {
+ source.readUtf8LineStrict();
+ fail();
+ } catch (EOFException expected) {
+ assertEquals("\\n not found: size=33 content=616161616161616162626262626262626363636363636363"
+ + "6464646464646464...", expected.getMessage());
+ }
+ }
+
+ @Test public void emptyLines() throws IOException {
+ data.writeUtf8("\n\n\n");
+ assertEquals("", source.readUtf8LineStrict());
+ assertEquals("", source.readUtf8LineStrict());
+ assertEquals("", source.readUtf8LineStrict());
+ assertTrue(source.exhausted());
+ }
+
+ @Test public void crDroppedPrecedingLf() throws IOException {
+ data.writeUtf8("abc\r\ndef\r\nghi\rjkl\r\n");
+ assertEquals("abc", source.readUtf8LineStrict());
+ assertEquals("def", source.readUtf8LineStrict());
+ assertEquals("ghi\rjkl", source.readUtf8LineStrict());
+ }
+
+ @Test public void bufferedReaderCompatible() throws IOException {
+ data.writeUtf8("abc\ndef");
+ assertEquals("abc", source.readUtf8Line());
+ assertEquals("def", source.readUtf8Line());
+ assertEquals(null, source.readUtf8Line());
+ }
+
+ @Test public void bufferedReaderCompatibleWithTrailingNewline() throws IOException {
+ data.writeUtf8("abc\ndef\n");
+ assertEquals("abc", source.readUtf8Line());
+ assertEquals("def", source.readUtf8Line());
+ assertEquals(null, source.readUtf8Line());
+ }
+}
diff --git a/okio/src/test/java/okio/RealBufferedSinkTest.java b/okio/okio/src/test/java/okio/RealBufferedSinkTest.java
similarity index 64%
rename from okio/src/test/java/okio/RealBufferedSinkTest.java
rename to okio/okio/src/test/java/okio/RealBufferedSinkTest.java
index 80a1317..808092d 100644
--- a/okio/src/test/java/okio/RealBufferedSinkTest.java
+++ b/okio/okio/src/test/java/okio/RealBufferedSinkTest.java
@@ -17,36 +17,31 @@
import java.io.IOException;
import java.io.OutputStream;
-import java.util.Arrays;
import org.junit.Test;
-import static okio.Util.UTF_8;
+import static okio.TestUtil.repeat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+/**
+ * Tests solely for the behavior of RealBufferedSink's implementation. For generic
+ * BufferedSink behavior use BufferedSinkTest.
+ */
public final class RealBufferedSinkTest {
- @Test public void outputStreamFromSink() throws Exception {
- OkBuffer sink = new OkBuffer();
- OutputStream out = new RealBufferedSink(sink).outputStream();
- out.write('a');
- out.write(repeat('b', 9998).getBytes(UTF_8));
- out.write('c');
- out.flush();
- assertEquals("a" + repeat('b', 9998) + "c", sink.readUtf8(10000));
- }
-
- @Test public void outputStreamFromSinkBounds() throws Exception {
- OkBuffer sink = new OkBuffer();
- OutputStream out = new RealBufferedSink(sink).outputStream();
+ @Test public void inputStreamCloses() throws Exception {
+ RealBufferedSink sink = new RealBufferedSink(new Buffer());
+ OutputStream out = sink.outputStream();
+ out.close();
try {
- out.write(new byte[100], 50, 51);
+ sink.writeUtf8("Hi!");
fail();
- } catch (ArrayIndexOutOfBoundsException expected) {
+ } catch (IllegalStateException e) {
+ assertEquals("closed", e.getMessage());
}
}
@Test public void bufferedSinkEmitsTailWhenItIsComplete() throws IOException {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeUtf8(repeat('a', Segment.SIZE - 1));
assertEquals(0, sink.size());
@@ -55,15 +50,8 @@
assertEquals(0, bufferedSink.buffer().size());
}
- @Test public void bufferedSinkEmitZero() throws IOException {
- OkBuffer sink = new OkBuffer();
- BufferedSink bufferedSink = new RealBufferedSink(sink);
- bufferedSink.writeUtf8("");
- assertEquals(0, sink.size());
- }
-
@Test public void bufferedSinkEmitMultipleSegments() throws IOException {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeUtf8(repeat('a', Segment.SIZE * 4 - 1));
assertEquals(Segment.SIZE * 3, sink.size());
@@ -71,7 +59,7 @@
}
@Test public void bufferedSinkFlush() throws IOException {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeByte('a');
assertEquals(0, sink.size());
@@ -81,7 +69,7 @@
}
@Test public void bytesEmittedToSinkWithFlush() throws Exception {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeUtf8("abc");
bufferedSink.flush();
@@ -89,34 +77,34 @@
}
@Test public void bytesNotEmittedToSinkWithoutFlush() throws Exception {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeUtf8("abc");
assertEquals(0, sink.size());
}
+ @Test public void bytesEmittedToSinkWithEmit() throws Exception {
+ Buffer sink = new Buffer();
+ BufferedSink bufferedSink = new RealBufferedSink(sink);
+ bufferedSink.writeUtf8("abc");
+ bufferedSink.emit();
+ assertEquals(3, sink.size());
+ }
+
@Test public void completeSegmentsEmitted() throws Exception {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeUtf8(repeat('a', Segment.SIZE * 3));
assertEquals(Segment.SIZE * 3, sink.size());
}
@Test public void incompleteSegmentsNotEmitted() throws Exception {
- OkBuffer sink = new OkBuffer();
+ Buffer sink = new Buffer();
BufferedSink bufferedSink = new RealBufferedSink(sink);
bufferedSink.writeUtf8(repeat('a', Segment.SIZE * 3 - 1));
assertEquals(Segment.SIZE * 2, sink.size());
}
- @Test public void closeEmitsBufferedBytes() throws IOException {
- OkBuffer sink = new OkBuffer();
- BufferedSink bufferedSink = new RealBufferedSink(sink);
- bufferedSink.writeByte('a');
- bufferedSink.close();
- assertEquals('a', sink.readByte());
- }
-
@Test public void closeWithExceptionWhenWriting() throws IOException {
MockSink mockSink = new MockSink();
mockSink.scheduleThrow(0, new IOException());
@@ -127,7 +115,7 @@
fail();
} catch (IOException expected) {
}
- mockSink.assertLog("write(OkBuffer[size=1 data=61], 1)", "close()");
+ mockSink.assertLog("write(Buffer[size=1 data=61], 1)", "close()");
}
@Test public void closeWithExceptionWhenClosing() throws IOException {
@@ -140,7 +128,7 @@
fail();
} catch (IOException expected) {
}
- mockSink.assertLog("write(OkBuffer[size=1 data=61], 1)", "close()");
+ mockSink.assertLog("write(Buffer[size=1 data=61], 1)", "close()");
}
@Test public void closeWithExceptionWhenWritingAndClosing() throws IOException {
@@ -155,7 +143,7 @@
} catch (IOException expected) {
assertEquals("first", expected.getMessage());
}
- mockSink.assertLog("write(OkBuffer[size=1 data=61], 1)", "close()");
+ mockSink.assertLog("write(Buffer[size=1 data=61], 1)", "close()");
}
@Test public void operationsAfterClose() throws IOException {
@@ -184,6 +172,12 @@
}
try {
+ bufferedSink.emit();
+ fail();
+ } catch (IllegalStateException expected) {
+ }
+
+ try {
bufferedSink.flush();
fail();
} catch (IllegalStateException expected) {
@@ -207,9 +201,44 @@
os.flush();
}
- private String repeat(char c, int count) {
- char[] array = new char[count];
- Arrays.fill(array, c);
- return new String(array);
- }
+ @Test public void writeAll() throws IOException {
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+
+ bufferedSink.buffer().writeUtf8("abc");
+ assertEquals(3, bufferedSink.writeAll(new Buffer().writeUtf8("def")));
+
+ assertEquals(6, bufferedSink.buffer().size());
+ assertEquals("abcdef", bufferedSink.buffer().readUtf8(6));
+ mockSink.assertLog(); // No writes.
+ }
+
+ @Test public void writeAllExhausted() throws IOException {
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+
+ assertEquals(0, bufferedSink.writeAll(new Buffer()));
+ assertEquals(0, bufferedSink.buffer().size());
+ mockSink.assertLog(); // No writes.
+ }
+
+ @Test public void writeAllWritesOneSegmentAtATime() throws IOException {
+ Buffer write1 = new Buffer().writeUtf8(TestUtil.repeat('a', Segment.SIZE));
+ Buffer write2 = new Buffer().writeUtf8(TestUtil.repeat('b', Segment.SIZE));
+ Buffer write3 = new Buffer().writeUtf8(TestUtil.repeat('c', Segment.SIZE));
+
+ Buffer source = new Buffer().writeUtf8(""
+ + TestUtil.repeat('a', Segment.SIZE)
+ + TestUtil.repeat('b', Segment.SIZE)
+ + TestUtil.repeat('c', Segment.SIZE));
+
+ MockSink mockSink = new MockSink();
+ BufferedSink bufferedSink = Okio.buffer(mockSink);
+ assertEquals(Segment.SIZE * 3, bufferedSink.writeAll(source));
+
+ mockSink.assertLog(
+ "write(" + write1 + ", " + write1.size() + ")",
+ "write(" + write2 + ", " + write2.size() + ")",
+ "write(" + write3 + ", " + write3.size() + ")");
+ }
}
diff --git a/okio/src/test/java/okio/RealBufferedSourceTest.java b/okio/okio/src/test/java/okio/RealBufferedSourceTest.java
similarity index 73%
rename from okio/src/test/java/okio/RealBufferedSourceTest.java
rename to okio/okio/src/test/java/okio/RealBufferedSourceTest.java
index a77eaf2..92ded30 100644
--- a/okio/src/test/java/okio/RealBufferedSourceTest.java
+++ b/okio/okio/src/test/java/okio/RealBufferedSourceTest.java
@@ -18,16 +18,20 @@
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
-import java.util.Arrays;
import org.junit.Test;
+import static okio.TestUtil.repeat;
import static okio.Util.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
+/**
+ * Tests solely for the behavior of RealBufferedSource's implementation. For generic
+ * BufferedSource behavior use BufferedSourceTest.
+ */
public final class RealBufferedSourceTest {
- @Test public void inputStreamFromSource() throws Exception {
- OkBuffer source = new OkBuffer();
+ @Test public void inputStreamTracksSegments() throws Exception {
+ Buffer source = new Buffer();
source.writeUtf8("a");
source.writeUtf8(repeat('b', Segment.SIZE));
source.writeUtf8("c");
@@ -62,19 +66,20 @@
assertEquals(0, source.size());
}
- @Test public void inputStreamFromSourceBounds() throws IOException {
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('a', 100));
- InputStream in = new RealBufferedSource(source).inputStream();
+ @Test public void inputStreamCloses() throws Exception {
+ RealBufferedSource source = new RealBufferedSource(new Buffer());
+ InputStream in = source.inputStream();
+ in.close();
try {
- in.read(new byte[100], 50, 51);
+ source.require(1);
fail();
- } catch (ArrayIndexOutOfBoundsException expected) {
+ } catch (IllegalStateException e) {
+ assertEquals("closed", e.getMessage());
}
}
@Test public void requireTracksBufferFirst() throws Exception {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
source.writeUtf8("bb");
BufferedSource bufferedSource = new RealBufferedSource(source);
@@ -86,7 +91,7 @@
}
@Test public void requireIncludesBufferBytes() throws Exception {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
source.writeUtf8("b");
BufferedSource bufferedSource = new RealBufferedSource(source);
@@ -97,7 +102,7 @@
}
@Test public void requireInsufficientData() throws Exception {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
source.writeUtf8("a");
BufferedSource bufferedSource = new RealBufferedSource(source);
@@ -110,7 +115,7 @@
}
@Test public void requireReadsOneSegmentAtATime() throws Exception {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
source.writeUtf8(repeat('a', Segment.SIZE));
source.writeUtf8(repeat('b', Segment.SIZE));
@@ -121,20 +126,8 @@
assertEquals(Segment.SIZE, bufferedSource.buffer().size());
}
- @Test public void skipInsufficientData() throws Exception {
- OkBuffer source = new OkBuffer();
- source.writeUtf8("a");
-
- BufferedSource bufferedSource = new RealBufferedSource(source);
- try {
- bufferedSource.skip(2);
- fail();
- } catch (EOFException expected) {
- }
- }
-
@Test public void skipReadsOneSegmentAtATime() throws Exception {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
source.writeUtf8(repeat('a', Segment.SIZE));
source.writeUtf8(repeat('b', Segment.SIZE));
BufferedSource bufferedSource = new RealBufferedSource(source);
@@ -144,7 +137,7 @@
}
@Test public void skipTracksBufferFirst() throws Exception {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
source.writeUtf8("bb");
BufferedSource bufferedSource = new RealBufferedSource(source);
@@ -156,7 +149,7 @@
}
@Test public void operationsAfterClose() throws IOException {
- OkBuffer source = new OkBuffer();
+ Buffer source = new Buffer();
BufferedSource bufferedSource = new RealBufferedSource(source);
bufferedSource.close();
@@ -200,9 +193,26 @@
}
}
- private String repeat(char c, int count) {
- char[] array = new char[count];
- Arrays.fill(array, c);
- return new String(array);
+ /**
+ * We don't want readAll to buffer an unbounded amount of data. Instead it
+ * should buffer a segment, write it, and repeat.
+ */
+ @Test public void readAllReadsOneSegmentAtATime() throws IOException {
+ Buffer write1 = new Buffer().writeUtf8(TestUtil.repeat('a', Segment.SIZE));
+ Buffer write2 = new Buffer().writeUtf8(TestUtil.repeat('b', Segment.SIZE));
+ Buffer write3 = new Buffer().writeUtf8(TestUtil.repeat('c', Segment.SIZE));
+
+ Buffer source = new Buffer().writeUtf8(""
+ + TestUtil.repeat('a', Segment.SIZE)
+ + TestUtil.repeat('b', Segment.SIZE)
+ + TestUtil.repeat('c', Segment.SIZE));
+
+ MockSink mockSink = new MockSink();
+ BufferedSource bufferedSource = Okio.buffer((Source) source);
+ assertEquals(Segment.SIZE * 3, bufferedSource.readAll(mockSink));
+ mockSink.assertLog(
+ "write(" + write1 + ", " + write1.size() + ")",
+ "write(" + write2 + ", " + write2.size() + ")",
+ "write(" + write3 + ", " + write3.size() + ")");
}
}
diff --git a/okio/okio/src/test/java/okio/SocketTimeoutTest.java b/okio/okio/src/test/java/okio/SocketTimeoutTest.java
new file mode 100644
index 0000000..6bb73e6
--- /dev/null
+++ b/okio/okio/src/test/java/okio/SocketTimeoutTest.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InterruptedIOException;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class SocketTimeoutTest {
+
+ // The size of the socket buffers to use. Less than half the data transferred during tests to
+ // ensure send and receive buffers are flooded and any necessary blocking behavior takes place.
+ private static final int SOCKET_BUFFER_SIZE = 256 * 1024;
+ private static final int ONE_MB = 1024 * 1024;
+
+ @Test public void readWithoutTimeout() throws Exception {
+ Socket socket = socket(ONE_MB, 0);
+ BufferedSource source = Okio.buffer(Okio.source(socket));
+ source.timeout().timeout(5000, TimeUnit.MILLISECONDS);
+ source.require(ONE_MB);
+ socket.close();
+ }
+
+ @Test public void readWithTimeout() throws Exception {
+ Socket socket = socket(0, 0);
+ BufferedSource source = Okio.buffer(Okio.source(socket));
+ source.timeout().timeout(250, TimeUnit.MILLISECONDS);
+ try {
+ source.require(ONE_MB);
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ socket.close();
+ }
+
+ @Test public void writeWithoutTimeout() throws Exception {
+ Socket socket = socket(0, ONE_MB);
+ Sink sink = Okio.buffer(Okio.sink(socket));
+ sink.timeout().timeout(500, TimeUnit.MILLISECONDS);
+ byte[] data = new byte[ONE_MB];
+ sink.write(new Buffer().write(data), data.length);
+ sink.flush();
+ socket.close();
+ }
+
+ @Test public void writeWithTimeout() throws Exception {
+ Socket socket = socket(0, 0);
+ Sink sink = Okio.sink(socket);
+ sink.timeout().timeout(500, TimeUnit.MILLISECONDS);
+ byte[] data = new byte[ONE_MB];
+ long start = System.nanoTime();
+ try {
+ sink.write(new Buffer().write(data), data.length);
+ sink.flush();
+ fail();
+ } catch (InterruptedIOException expected) {
+ }
+ long elapsed = System.nanoTime() - start;
+ socket.close();
+
+ assertTrue("elapsed: " + elapsed, TimeUnit.NANOSECONDS.toMillis(elapsed) >= 500);
+ assertTrue("elapsed: " + elapsed, TimeUnit.NANOSECONDS.toMillis(elapsed) <= 750);
+ }
+
+ /**
+ * Returns a socket that can read {@code readableByteCount} incoming bytes and
+ * will accept {@code writableByteCount} written bytes. The socket will idle
+ * for 5 seconds when the required data has been read and written.
+ */
+ static Socket socket(final int readableByteCount, final int writableByteCount) throws IOException {
+ final ServerSocket serverSocket = new ServerSocket(0);
+ serverSocket.setReuseAddress(true);
+ serverSocket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+
+ Thread peer = new Thread("peer") {
+ @Override public void run() {
+ Socket socket = null;
+ try {
+ socket = serverSocket.accept();
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ writeFully(socket.getOutputStream(), readableByteCount);
+ readFully(socket.getInputStream(), writableByteCount);
+ Thread.sleep(5000); // Sleep 5 seconds so the peer can close the connection.
+ } catch (Exception ignored) {
+ } finally {
+ try {
+ if (socket != null) socket.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ };
+ peer.start();
+
+ Socket socket = new Socket(serverSocket.getInetAddress(), serverSocket.getLocalPort());
+ socket.setReceiveBufferSize(SOCKET_BUFFER_SIZE);
+ socket.setSendBufferSize(SOCKET_BUFFER_SIZE);
+ return socket;
+ }
+
+ private static void writeFully(OutputStream out, int byteCount) throws IOException {
+ out.write(new byte[byteCount]);
+ out.flush();
+ }
+
+ private static byte[] readFully(InputStream in, int byteCount) throws IOException {
+ int count = 0;
+ byte[] result = new byte[byteCount];
+ while (count < byteCount) {
+ int read = in.read(result, count, result.length - count);
+ if (read == -1) throw new EOFException();
+ count += read;
+ }
+ return result;
+ }
+}
diff --git a/okio/okio/src/test/java/okio/TestUtil.java b/okio/okio/src/test/java/okio/TestUtil.java
new file mode 100644
index 0000000..e3a778f
--- /dev/null
+++ b/okio/okio/src/test/java/okio/TestUtil.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import java.util.Arrays;
+import java.util.Random;
+
+import static org.junit.Assert.assertEquals;
+
+final class TestUtil {
+ private TestUtil() {
+ }
+
+ static void assertByteArraysEquals(byte[] a, byte[] b) {
+ assertEquals(Arrays.toString(a), Arrays.toString(b));
+ }
+
+ static void assertByteArrayEquals(String expectedUtf8, byte[] b) {
+ assertEquals(expectedUtf8, new String(b, Util.UTF_8));
+ }
+
+ static ByteString randomBytes(int length) {
+ Random random = new Random(0);
+ byte[] randomBytes = new byte[length];
+ random.nextBytes(randomBytes);
+ return ByteString.of(randomBytes);
+ }
+
+ static String repeat(char c, int count) {
+ char[] array = new char[count];
+ Arrays.fill(array, c);
+ return new String(array);
+ }
+}
diff --git a/okio/okio/src/test/java/okio/Utf8Test.java b/okio/okio/src/test/java/okio/Utf8Test.java
new file mode 100644
index 0000000..5456607
--- /dev/null
+++ b/okio/okio/src/test/java/okio/Utf8Test.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2014 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 okio;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+public final class Utf8Test {
+ @Test public void oneByteCharacters() throws Exception {
+ assertEncoded("00", 0x00); // Smallest 1-byte character.
+ assertEncoded("20", ' ');
+ assertEncoded("7e", '~');
+ assertEncoded("7f", 0x7f); // Largest 1-byte character.
+ }
+
+ @Test public void twoByteCharacters() throws Exception {
+ assertEncoded("c280", 0x0080); // Smallest 2-byte character.
+ assertEncoded("c3bf", 0x00ff);
+ assertEncoded("c480", 0x0100);
+ assertEncoded("dfbf", 0x07ff); // Largest 2-byte character.
+ }
+
+ @Test public void threeByteCharacters() throws Exception {
+ assertEncoded("e0a080", 0x0800); // Smallest 3-byte character.
+ assertEncoded("e0bfbf", 0x0fff);
+ assertEncoded("e18080", 0x1000);
+ assertEncoded("e1bfbf", 0x1fff);
+ assertEncoded("ed8080", 0xd000);
+ assertEncoded("ed9fbf", 0xd7ff); // Largest character lower than the min surrogate.
+ assertEncoded("ee8080", 0xe000); // Smallest character greater than the max surrogate.
+ assertEncoded("eebfbf", 0xefff);
+ assertEncoded("ef8080", 0xf000);
+ assertEncoded("efbfbf", 0xffff); // Largest 3-byte character.
+ }
+
+ @Test public void fourByteCharacters() throws Exception {
+ assertEncoded("f0908080", 0x010000); // Smallest surrogate pair.
+ assertEncoded("f48fbfbf", 0x10ffff); // Largest code point expressible by UTF-16.
+ }
+
+ @Test public void danglingHighSurrogate() throws Exception {
+ assertEncoded("3f", "\ud800"); // "?"
+ }
+
+ @Test public void lowSurrogateWithoutHighSurrogate() throws Exception {
+ assertEncoded("3f", "\udc00"); // "?"
+ }
+
+ @Test public void highSurrogateFollowedByNonSurrogate() throws Exception {
+ assertEncoded("3f61", "\ud800\u0061"); // "?a": Following character is too low.
+ assertEncoded("3fee8080", "\ud800\ue000"); // "?\ue000": Following character is too high.
+ }
+
+ @Test public void multipleSegmentString() throws Exception {
+ String a = TestUtil.repeat('a', Segment.SIZE + Segment.SIZE + 1);
+ Buffer encoded = new Buffer().writeUtf8(a);
+ Buffer expected = new Buffer().write(a.getBytes(Util.UTF_8));
+ assertEquals(expected, encoded);
+ }
+
+ @Test public void stringSpansSegments() throws Exception {
+ Buffer buffer = new Buffer();
+ String a = TestUtil.repeat('a', Segment.SIZE - 1);
+ String b = "bb";
+ String c = TestUtil.repeat('c', Segment.SIZE - 1);
+ buffer.writeUtf8(a);
+ buffer.writeUtf8(b);
+ buffer.writeUtf8(c);
+ assertEquals(a + b + c, buffer.readUtf8());
+ }
+
+ private void assertEncoded(String hex, int... codePoints) throws Exception {
+ assertEncoded(hex, new String(codePoints, 0, codePoints.length));
+ }
+
+ private void assertEncoded(String hex, String string) throws Exception {
+ ByteString expectedUtf8 = ByteString.decodeHex(hex);
+
+ // Confirm our expectations are consistent with the platform.
+ ByteString platformUtf8 = ByteString.of(string.getBytes("UTF-8"));
+ assertEquals(expectedUtf8, platformUtf8);
+
+ // Confirm our implementation matches those expectations.
+ ByteString actualUtf8 = new Buffer().writeUtf8(string).readByteString();
+ assertEquals(expectedUtf8, actualUtf8);
+ }
+}
diff --git a/okio/pom.xml b/okio/pom.xml
index f1a33b5..4467f5f 100644
--- a/okio/pom.xml
+++ b/okio/pom.xml
@@ -4,26 +4,105 @@
<modelVersion>4.0.0</modelVersion>
<parent>
- <groupId>com.squareup.okhttp</groupId>
- <artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <groupId>org.sonatype.oss</groupId>
+ <artifactId>oss-parent</artifactId>
+ <version>7</version>
</parent>
<groupId>com.squareup.okio</groupId>
- <artifactId>okio</artifactId>
- <name>Okio</name>
+ <artifactId>okio-parent</artifactId>
+ <version>1.3.0-SNAPSHOT</version>
+ <packaging>pom</packaging>
+ <name>Okio (Parent)</name>
+ <description>A modern I/O API for Java</description>
+ <url>https://github.com/square/okio</url>
- <dependencies>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.codehaus.mojo</groupId>
- <artifactId>animal-sniffer-annotations</artifactId>
- <version>1.10</version>
- <optional>true</optional>
- </dependency>
- </dependencies>
+ <modules>
+ <module>okio</module>
+ <module>benchmarks</module>
+ </modules>
+
+ <properties>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <java.version>1.7</java.version>
+
+ <!-- Dependencies -->
+ <animal.sniffer.version>1.10</animal.sniffer.version>
+ <jmh.version>1.4.1</jmh.version>
+
+ <!-- Test Dependencies -->
+ <junit.version>4.11</junit.version>
+ </properties>
+
+ <scm>
+ <url>https://github.com/square/okio/</url>
+ <connection>scm:git:https://github.com/square/okio.git</connection>
+ <developerConnection>scm:git:git@github.com:square/okio.git</developerConnection>
+ <tag>HEAD</tag>
+ </scm>
+
+ <issueManagement>
+ <system>GitHub Issues</system>
+ <url>https://github.com/square/okio/issues</url>
+ </issueManagement>
+
+ <licenses>
+ <license>
+ <name>Apache 2.0</name>
+ <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
+ </license>
+ </licenses>
+
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>animal-sniffer-annotations</artifactId>
+ <version>${animal.sniffer.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.openjdk.jmh</groupId>
+ <artifactId>jmh-core</artifactId>
+ <version>${jmh.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.openjdk.jmh</groupId>
+ <artifactId>jmh-generator-annprocess</artifactId>
+ <version>${jmh.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.0</version>
+ <configuration>
+ <source>${java.version}</source>
+ <target>${java.version}</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-release-plugin</artifactId>
+ <version>2.5</version>
+ <configuration>
+ <autoVersionSubmodules>true</autoVersionSubmodules>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
</project>
diff --git a/okio/src/main/java/okio/BufferedSource.java b/okio/src/main/java/okio/BufferedSource.java
deleted file mode 100644
index 2b48823..0000000
--- a/okio/src/main/java/okio/BufferedSource.java
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-/**
- * A source that keeps a buffer internally so that callers can do small reads
- * without a performance penalty. It also allows clients to read ahead,
- * buffering as much as necessary before consuming input.
- */
-public interface BufferedSource extends Source {
- /** Returns this source's internal buffer. */
- OkBuffer buffer();
-
- /**
- * Returns true if there are no more bytes in this source. This will block
- * until there are bytes to read or the source is definitely exhausted.
- */
- boolean exhausted() throws IOException;
-
- /**
- * Returns when the buffer contains at least {@code byteCount} bytes. Throws
- * an {@link java.io.EOFException} if the source is exhausted before the
- * required bytes can be read.
- */
- void require(long byteCount) throws IOException;
-
- /** Removes a byte from this source and returns it. */
- byte readByte() throws IOException;
-
- /** Removes two bytes from this source and returns a big-endian short. */
- short readShort() throws IOException;
-
- /** Removes two bytes from this source and returns a little-endian short. */
- short readShortLe() throws IOException;
-
- /** Removes four bytes from this source and returns a big-endian int. */
- int readInt() throws IOException;
-
- /** Removes four bytes from this source and returns a little-endian int. */
- int readIntLe() throws IOException;
-
- /** Removes eight bytes from this source and returns a big-endian long. */
- long readLong() throws IOException;
-
- /** Removes eight bytes from this source and returns a little-endian long. */
- long readLongLe() throws IOException;
-
- /**
- * Reads and discards {@code byteCount} bytes from this source. Throws an
- * {@link java.io.EOFException} if the source is exhausted before the
- * requested bytes can be skipped.
- */
- void skip(long byteCount) throws IOException;
-
- /** Removes {@code byteCount} bytes from this and returns them as a byte string. */
- ByteString readByteString(long byteCount) throws IOException;
-
- /**
- * Removes {@code byteCount} bytes from this, decodes them as UTF-8 and
- * returns the string.
- */
- String readUtf8(long byteCount) throws IOException;
-
- /**
- * Removes and returns characters up to but not including the next line break.
- * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
- * not included in the result.
- *
- * <p><strong>On the end of the stream this method returns null,</strong> just
- * like {@link java.io.BufferedReader}. If the source doesn't end with a line
- * break then an implicit line break is assumed. Null is returned once the
- * source is exhausted. Use this for human-generated data, where a trailing
- * line break is optional.
- */
- String readUtf8Line() throws IOException;
-
- /**
- * Removes and returns characters up to but not including the next line break.
- * A line break is either {@code "\n"} or {@code "\r\n"}; these characters are
- * not included in the result.
- *
- * <p><strong>On the end of the stream this method throws.</strong> Every call
- * must consume either '\r\n' or '\n'. If these characters are absent in the
- * stream, an {@link java.io.EOFException} is thrown. Use this for
- * machine-generated data where a missing line break implies truncated input.
- */
- String readUtf8LineStrict() throws IOException;
-
- /**
- * Returns the index of {@code b} in the buffer, refilling it if necessary
- * until it is found. This reads an unbounded number of bytes into the buffer.
- * Returns -1 if the stream is exhausted before the requested byte is found.
- */
- long indexOf(byte b) throws IOException;
-
- /** Returns an input stream that reads from this source. */
- InputStream inputStream();
-}
diff --git a/okio/src/main/java/okio/Deadline.java b/okio/src/main/java/okio/Deadline.java
deleted file mode 100644
index e5f951e..0000000
--- a/okio/src/main/java/okio/Deadline.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.IOException;
-import java.io.InterruptedIOException;
-import java.util.concurrent.TimeUnit;
-
-/**
- * The time that a requested operation is due. If the deadline is reached before
- * the operation has completed, the operation should be aborted.
- */
-public class Deadline {
- public static final Deadline NONE = new Deadline() {
- @Override public Deadline start(long timeout, TimeUnit unit) {
- throw new UnsupportedOperationException();
- }
-
- @Override public boolean reached() {
- return false;
- }
- };
-
- private long deadlineNanos;
-
- public Deadline() {
- }
-
- public Deadline start(long timeout, TimeUnit unit) {
- deadlineNanos = System.nanoTime() + unit.toNanos(timeout);
- return this;
- }
-
- public boolean reached() {
- return System.nanoTime() - deadlineNanos >= 0; // Subtract to avoid overflow!
- }
-
- public final void throwIfReached() throws IOException {
- // TODO: a more catchable exception type?
- if (reached()) throw new IOException("Deadline reached");
-
- // If the thread is interrupted, do not proceed with further I/O.
- if (Thread.interrupted()) throw new InterruptedIOException();
- }
-}
diff --git a/okio/src/main/java/okio/OkBuffer.java b/okio/src/main/java/okio/OkBuffer.java
deleted file mode 100644
index 8dc4290..0000000
--- a/okio/src/main/java/okio/OkBuffer.java
+++ /dev/null
@@ -1,745 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-import static okio.Util.UTF_8;
-import static okio.Util.checkOffsetAndCount;
-import static okio.Util.reverseBytesLong;
-
-/**
- * A collection of bytes in memory.
- *
- * <p><strong>Moving data from one OkBuffer to another is fast.</strong> Instead
- * of copying bytes from one place in memory to another, this class just changes
- * ownership of the underlying byte arrays.
- *
- * <p><strong>This buffer grows with your data.</strong> Just like ArrayList,
- * each OkBuffer starts small. It consumes only the memory it needs to.
- *
- * <p><strong>This buffer pools its byte arrays.</strong> When you allocate a
- * byte array in Java, the runtime must zero-fill the requested array before
- * returning it to you. Even if you're going to write over that space anyway.
- * This class avoids zero-fill and GC churn by pooling byte arrays.
- */
-public final class OkBuffer implements BufferedSource, BufferedSink, Cloneable {
- Segment head;
- long size;
-
- public OkBuffer() {
- }
-
- /** Returns the number of bytes currently in this buffer. */
- public long size() {
- return size;
- }
-
- @Override public OkBuffer buffer() {
- return this;
- }
-
- @Override public OutputStream outputStream() {
- return new OutputStream() {
- @Override public void write(int b) {
- writeByte((byte) b);
- }
-
- @Override public void write(byte[] data, int offset, int byteCount) {
- OkBuffer.this.write(data, offset, byteCount);
- }
-
- @Override public void flush() {
- }
-
- @Override public void close() {
- }
-
- @Override public String toString() {
- return this + ".outputStream()";
- }
- };
- }
-
- @Override public OkBuffer emitCompleteSegments() {
- return this; // Nowhere to emit to!
- }
-
- @Override public boolean exhausted() {
- return size == 0;
- }
-
- @Override public void require(long byteCount) throws EOFException {
- if (this.size < byteCount) throw new EOFException();
- }
-
- @Override public InputStream inputStream() {
- return new InputStream() {
- @Override public int read() {
- return readByte() & 0xff;
- }
-
- @Override public int read(byte[] sink, int offset, int byteCount) {
- return OkBuffer.this.read(sink, offset, byteCount);
- }
-
- @Override public int available() {
- return (int) Math.min(size, Integer.MAX_VALUE);
- }
-
- @Override public void close() {
- }
-
- @Override public String toString() {
- return OkBuffer.this + ".inputStream()";
- }
- };
- }
-
- /**
- * Returns the number of bytes in segments that are not writable. This is the
- * number of bytes that can be flushed immediately to an underlying sink
- * without harming throughput.
- */
- public long completeSegmentByteCount() {
- long result = size;
- if (result == 0) return 0;
-
- // Omit the tail if it's still writable.
- Segment tail = head.prev;
- if (tail.limit < Segment.SIZE) {
- result -= tail.limit - tail.pos;
- }
-
- return result;
- }
-
- @Override public byte readByte() {
- if (size == 0) throw new IllegalStateException("size == 0");
-
- Segment segment = head;
- int pos = segment.pos;
- int limit = segment.limit;
-
- byte[] data = segment.data;
- byte b = data[pos++];
- size -= 1;
-
- if (pos == limit) {
- head = segment.pop();
- SegmentPool.INSTANCE.recycle(segment);
- } else {
- segment.pos = pos;
- }
-
- return b;
- }
-
- /** Returns the byte at {@code pos}. */
- public byte getByte(long pos) {
- checkOffsetAndCount(size, pos, 1);
- for (Segment s = head; true; s = s.next) {
- int segmentByteCount = s.limit - s.pos;
- if (pos < segmentByteCount) return s.data[s.pos + (int) pos];
- pos -= segmentByteCount;
- }
- }
-
- @Override public short readShort() {
- if (size < 2) throw new IllegalStateException("size < 2: " + size);
-
- Segment segment = head;
- int pos = segment.pos;
- int limit = segment.limit;
-
- // If the short is split across multiple segments, delegate to readByte().
- if (limit - pos < 2) {
- int s = (readByte() & 0xff) << 8
- | (readByte() & 0xff);
- return (short) s;
- }
-
- byte[] data = segment.data;
- int s = (data[pos++] & 0xff) << 8
- | (data[pos++] & 0xff);
- size -= 2;
-
- if (pos == limit) {
- head = segment.pop();
- SegmentPool.INSTANCE.recycle(segment);
- } else {
- segment.pos = pos;
- }
-
- return (short) s;
- }
-
- @Override public int readInt() {
- if (size < 4) throw new IllegalStateException("size < 4: " + size);
-
- Segment segment = head;
- int pos = segment.pos;
- int limit = segment.limit;
-
- // If the int is split across multiple segments, delegate to readByte().
- if (limit - pos < 4) {
- return (readByte() & 0xff) << 24
- | (readByte() & 0xff) << 16
- | (readByte() & 0xff) << 8
- | (readByte() & 0xff);
- }
-
- byte[] data = segment.data;
- int i = (data[pos++] & 0xff) << 24
- | (data[pos++] & 0xff) << 16
- | (data[pos++] & 0xff) << 8
- | (data[pos++] & 0xff);
- size -= 4;
-
- if (pos == limit) {
- head = segment.pop();
- SegmentPool.INSTANCE.recycle(segment);
- } else {
- segment.pos = pos;
- }
-
- return i;
- }
-
- @Override public long readLong() {
- if (size < 8) throw new IllegalStateException("size < 8: " + size);
-
- Segment segment = head;
- int pos = segment.pos;
- int limit = segment.limit;
-
- // If the long is split across multiple segments, delegate to readInt().
- if (limit - pos < 8) {
- return (readInt() & 0xffffffffL) << 32
- | (readInt() & 0xffffffffL);
- }
-
- byte[] data = segment.data;
- long v = (data[pos++] & 0xffL) << 56
- | (data[pos++] & 0xffL) << 48
- | (data[pos++] & 0xffL) << 40
- | (data[pos++] & 0xffL) << 32
- | (data[pos++] & 0xffL) << 24
- | (data[pos++] & 0xffL) << 16
- | (data[pos++] & 0xffL) << 8
- | (data[pos++] & 0xffL);
- size -= 8;
-
- if (pos == limit) {
- head = segment.pop();
- SegmentPool.INSTANCE.recycle(segment);
- } else {
- segment.pos = pos;
- }
-
- return v;
- }
-
- @Override public short readShortLe() {
- return Util.reverseBytesShort(readShort());
- }
-
- @Override public int readIntLe() {
- return Util.reverseBytesInt(readInt());
- }
-
- @Override public long readLongLe() {
- return Util.reverseBytesLong(readLong());
- }
-
- @Override public ByteString readByteString(long byteCount) {
- return new ByteString(readBytes(byteCount));
- }
-
- @Override public String readUtf8(long byteCount) {
- checkOffsetAndCount(this.size, 0, byteCount);
- if (byteCount > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
- }
- if (byteCount == 0) return "";
-
- Segment head = this.head;
- if (head.pos + byteCount > head.limit) {
- // If the string spans multiple segments, delegate to readBytes().
- return new String(readBytes(byteCount), Util.UTF_8);
- }
-
- String result = new String(head.data, head.pos, (int) byteCount, UTF_8);
- head.pos += byteCount;
- this.size -= byteCount;
-
- if (head.pos == head.limit) {
- this.head = head.pop();
- SegmentPool.INSTANCE.recycle(head);
- }
-
- return result;
- }
-
- @Override public String readUtf8Line() throws IOException {
- long newline = indexOf((byte) '\n');
-
- if (newline == -1) {
- return size != 0 ? readUtf8(size) : null;
- }
-
- return readUtf8Line(newline);
- }
-
- @Override public String readUtf8LineStrict() throws IOException {
- long newline = indexOf((byte) '\n');
- if (newline == -1) throw new EOFException();
- return readUtf8Line(newline);
- }
-
- String readUtf8Line(long newline) {
- if (newline > 0 && getByte(newline - 1) == '\r') {
- // Read everything until '\r\n', then skip the '\r\n'.
- String result = readUtf8((newline - 1));
- skip(2);
- return result;
-
- } else {
- // Read everything until '\n', then skip the '\n'.
- String result = readUtf8(newline);
- skip(1);
- return result;
- }
- }
-
- private byte[] readBytes(long byteCount) {
- checkOffsetAndCount(this.size, 0, byteCount);
- if (byteCount > Integer.MAX_VALUE) {
- throw new IllegalArgumentException("byteCount > Integer.MAX_VALUE: " + byteCount);
- }
-
- int offset = 0;
- byte[] result = new byte[(int) byteCount];
-
- while (offset < byteCount) {
- int toCopy = (int) Math.min(byteCount - offset, head.limit - head.pos);
- System.arraycopy(head.data, head.pos, result, offset, toCopy);
-
- offset += toCopy;
- head.pos += toCopy;
-
- if (head.pos == head.limit) {
- Segment toRecycle = head;
- head = toRecycle.pop();
- SegmentPool.INSTANCE.recycle(toRecycle);
- }
- }
-
- this.size -= byteCount;
- return result;
- }
-
- /** Like {@link InputStream#read}. */
- int read(byte[] sink, int offset, int byteCount) {
- Segment s = this.head;
- if (s == null) return -1;
- int toCopy = Math.min(byteCount, s.limit - s.pos);
- System.arraycopy(s.data, s.pos, sink, offset, toCopy);
-
- s.pos += toCopy;
- this.size -= toCopy;
-
- if (s.pos == s.limit) {
- this.head = s.pop();
- SegmentPool.INSTANCE.recycle(s);
- }
-
- return toCopy;
- }
-
- /**
- * Discards all bytes in this buffer. Calling this method when you're done
- * with a buffer will return its segments to the pool.
- */
- public void clear() {
- skip(size);
- }
-
- /** Discards {@code byteCount} bytes from the head of this buffer. */
- @Override public void skip(long byteCount) {
- checkOffsetAndCount(this.size, 0, byteCount);
-
- this.size -= byteCount;
- while (byteCount > 0) {
- int toSkip = (int) Math.min(byteCount, head.limit - head.pos);
- byteCount -= toSkip;
- head.pos += toSkip;
-
- if (head.pos == head.limit) {
- Segment toRecycle = head;
- head = toRecycle.pop();
- SegmentPool.INSTANCE.recycle(toRecycle);
- }
- }
- }
-
- @Override public OkBuffer write(ByteString byteString) {
- return write(byteString.data, 0, byteString.data.length);
- }
-
- @Override public OkBuffer writeUtf8(String string) {
- // TODO: inline UTF-8 encoding to save allocating a byte[]?
- byte[] data = string.getBytes(Util.UTF_8);
- return write(data, 0, data.length);
- }
-
- @Override public OkBuffer write(byte[] source) {
- return write(source, 0, source.length);
- }
-
- @Override public OkBuffer write(byte[] source, int offset, int byteCount) {
- int limit = offset + byteCount;
- while (offset < limit) {
- Segment tail = writableSegment(1);
-
- int toCopy = Math.min(limit - offset, Segment.SIZE - tail.limit);
- System.arraycopy(source, offset, tail.data, tail.limit, toCopy);
-
- offset += toCopy;
- tail.limit += toCopy;
- }
-
- this.size += byteCount;
- return this;
- }
-
- @Override public OkBuffer writeByte(int b) {
- Segment tail = writableSegment(1);
- tail.data[tail.limit++] = (byte) b;
- size += 1;
- return this;
- }
-
- @Override public OkBuffer writeShort(int s) {
- Segment tail = writableSegment(2);
- byte[] data = tail.data;
- int limit = tail.limit;
- data[limit++] = (byte) ((s >>> 8) & 0xff);
- data[limit++] = (byte) (s & 0xff);
- tail.limit = limit;
- size += 2;
- return this;
- }
-
- @Override public BufferedSink writeShortLe(int s) {
- return writeShort(Util.reverseBytesShort((short) s));
- }
-
- @Override public OkBuffer writeInt(int i) {
- Segment tail = writableSegment(4);
- byte[] data = tail.data;
- int limit = tail.limit;
- data[limit++] = (byte) ((i >>> 24) & 0xff);
- data[limit++] = (byte) ((i >>> 16) & 0xff);
- data[limit++] = (byte) ((i >>> 8) & 0xff);
- data[limit++] = (byte) (i & 0xff);
- tail.limit = limit;
- size += 4;
- return this;
- }
-
- @Override public BufferedSink writeIntLe(int i) {
- return writeInt(Util.reverseBytesInt(i));
- }
-
- @Override public OkBuffer writeLong(long v) {
- Segment tail = writableSegment(8);
- byte[] data = tail.data;
- int limit = tail.limit;
- data[limit++] = (byte) ((v >>> 56L) & 0xff);
- data[limit++] = (byte) ((v >>> 48L) & 0xff);
- data[limit++] = (byte) ((v >>> 40L) & 0xff);
- data[limit++] = (byte) ((v >>> 32L) & 0xff);
- data[limit++] = (byte) ((v >>> 24L) & 0xff);
- data[limit++] = (byte) ((v >>> 16L) & 0xff);
- data[limit++] = (byte) ((v >>> 8L) & 0xff);
- data[limit++] = (byte) (v & 0xff);
- tail.limit = limit;
- size += 8;
- return this;
- }
-
- @Override public BufferedSink writeLongLe(long v) {
- return writeLong(reverseBytesLong(v));
- }
-
- /**
- * Returns a tail segment that we can write at least {@code minimumCapacity}
- * bytes to, creating it if necessary.
- */
- Segment writableSegment(int minimumCapacity) {
- if (minimumCapacity < 1 || minimumCapacity > Segment.SIZE) throw new IllegalArgumentException();
-
- if (head == null) {
- head = SegmentPool.INSTANCE.take(); // Acquire a first segment.
- return head.next = head.prev = head;
- }
-
- Segment tail = head.prev;
- if (tail.limit + minimumCapacity > Segment.SIZE) {
- tail = tail.push(SegmentPool.INSTANCE.take()); // Append a new empty segment to fill up.
- }
- return tail;
- }
-
- @Override public void write(OkBuffer source, long byteCount) {
- // Move bytes from the head of the source buffer to the tail of this buffer
- // while balancing two conflicting goals: don't waste CPU and don't waste
- // memory.
- //
- //
- // Don't waste CPU (ie. don't copy data around).
- //
- // Copying large amounts of data is expensive. Instead, we prefer to
- // reassign entire segments from one OkBuffer to the other.
- //
- //
- // Don't waste memory.
- //
- // As an invariant, adjacent pairs of segments in an OkBuffer should be at
- // least 50% full, except for the head segment and the tail segment.
- //
- // The head segment cannot maintain the invariant because the application is
- // consuming bytes from this segment, decreasing its level.
- //
- // The tail segment cannot maintain the invariant because the application is
- // producing bytes, which may require new nearly-empty tail segments to be
- // appended.
- //
- //
- // Moving segments between buffers
- //
- // When writing one buffer to another, we prefer to reassign entire segments
- // over copying bytes into their most compact form. Suppose we have a buffer
- // with these segment levels [91%, 61%]. If we append a buffer with a
- // single [72%] segment, that yields [91%, 61%, 72%]. No bytes are copied.
- //
- // Or suppose we have a buffer with these segment levels: [100%, 2%], and we
- // want to append it to a buffer with these segment levels [99%, 3%]. This
- // operation will yield the following segments: [100%, 2%, 99%, 3%]. That
- // is, we do not spend time copying bytes around to achieve more efficient
- // memory use like [100%, 100%, 4%].
- //
- // When combining buffers, we will compact adjacent buffers when their
- // combined level doesn't exceed 100%. For example, when we start with
- // [100%, 40%] and append [30%, 80%], the result is [100%, 70%, 80%].
- //
- //
- // Splitting segments
- //
- // Occasionally we write only part of a source buffer to a sink buffer. For
- // example, given a sink [51%, 91%], we may want to write the first 30% of
- // a source [92%, 82%] to it. To simplify, we first transform the source to
- // an equivalent buffer [30%, 62%, 82%] and then move the head segment,
- // yielding sink [51%, 91%, 30%] and source [62%, 82%].
-
- if (source == this) {
- throw new IllegalArgumentException("source == this");
- }
- checkOffsetAndCount(source.size, 0, byteCount);
-
- while (byteCount > 0) {
- // Is a prefix of the source's head segment all that we need to move?
- if (byteCount < (source.head.limit - source.head.pos)) {
- Segment tail = head != null ? head.prev : null;
- if (tail == null || byteCount + (tail.limit - tail.pos) > Segment.SIZE) {
- // We're going to need another segment. Split the source's head
- // segment in two, then move the first of those two to this buffer.
- source.head = source.head.split((int) byteCount);
- } else {
- // Our existing segments are sufficient. Move bytes from source's head to our tail.
- source.head.writeTo(tail, (int) byteCount);
- source.size -= byteCount;
- this.size += byteCount;
- return;
- }
- }
-
- // Remove the source's head segment and append it to our tail.
- Segment segmentToMove = source.head;
- long movedByteCount = segmentToMove.limit - segmentToMove.pos;
- source.head = segmentToMove.pop();
- if (head == null) {
- head = segmentToMove;
- head.next = head.prev = head;
- } else {
- Segment tail = head.prev;
- tail = tail.push(segmentToMove);
- tail.compact();
- }
- source.size -= movedByteCount;
- this.size += movedByteCount;
- byteCount -= movedByteCount;
- }
- }
-
- @Override public long read(OkBuffer sink, long byteCount) {
- if (this.size == 0) return -1L;
- if (byteCount > this.size) byteCount = this.size;
- sink.write(this, byteCount);
- return byteCount;
- }
-
- @Override public OkBuffer deadline(Deadline deadline) {
- // All operations are in memory so this class doesn't need to honor deadlines.
- return this;
- }
-
- @Override public long indexOf(byte b) {
- return indexOf(b, 0);
- }
-
- /**
- * Returns the index of {@code b} in this at or beyond {@code fromIndex}, or
- * -1 if this buffer does not contain {@code b} in that range.
- */
- public long indexOf(byte b, long fromIndex) {
- Segment s = head;
- if (s == null) return -1L;
- long offset = 0L;
- do {
- int segmentByteCount = s.limit - s.pos;
- if (fromIndex > segmentByteCount) {
- fromIndex -= segmentByteCount;
- } else {
- byte[] data = s.data;
- for (long pos = s.pos + fromIndex, limit = s.limit; pos < limit; pos++) {
- if (data[(int) pos] == b) return offset + pos - s.pos;
- }
- fromIndex = 0;
- }
- offset += segmentByteCount;
- s = s.next;
- } while (s != head);
- return -1L;
- }
-
- @Override public void flush() {
- }
-
- @Override public void close() {
- }
-
- /** For testing. This returns the sizes of the segments in this buffer. */
- List<Integer> segmentSizes() {
- if (head == null) return Collections.emptyList();
- List<Integer> result = new ArrayList<Integer>();
- result.add(head.limit - head.pos);
- for (Segment s = head.next; s != head; s = s.next) {
- result.add(s.limit - s.pos);
- }
- return result;
- }
-
- @Override public boolean equals(Object o) {
- if (!(o instanceof OkBuffer)) return false;
- OkBuffer that = (OkBuffer) o;
- if (size != that.size) return false;
- if (size == 0) return true; // Both buffers are empty.
-
- Segment sa = this.head;
- Segment sb = that.head;
- int posA = sa.pos;
- int posB = sb.pos;
-
- for (long pos = 0, count; pos < size; pos += count) {
- count = Math.min(sa.limit - posA, sb.limit - posB);
-
- for (int i = 0; i < count; i++) {
- if (sa.data[posA++] != sb.data[posB++]) return false;
- }
-
- if (posA == sa.limit) {
- sa = sa.next;
- posA = sa.pos;
- }
-
- if (posB == sb.limit) {
- sb = sb.next;
- posB = sb.pos;
- }
- }
-
- return true;
- }
-
- @Override public int hashCode() {
- Segment s = head;
- if (s == null) return 0;
- int result = 1;
- do {
- for (int pos = s.pos, limit = s.limit; pos < limit; pos++) {
- result = 31 * result + s.data[pos];
- }
- s = s.next;
- } while (s != head);
- return result;
- }
-
- @Override public String toString() {
- if (size == 0) {
- return "OkBuffer[size=0]";
- }
-
- if (size <= 16) {
- ByteString data = clone().readByteString(size);
- return String.format("OkBuffer[size=%s data=%s]", size, data.hex());
- }
-
- try {
- MessageDigest md5 = MessageDigest.getInstance("MD5");
- md5.update(head.data, head.pos, head.limit - head.pos);
- for (Segment s = head.next; s != head; s = s.next) {
- md5.update(s.data, s.pos, s.limit - s.pos);
- }
- return String.format("OkBuffer[size=%s md5=%s]",
- size, ByteString.of(md5.digest()).hex());
- } catch (NoSuchAlgorithmException e) {
- throw new AssertionError();
- }
- }
-
- /** Returns a deep copy of this buffer. */
- @Override public OkBuffer clone() {
- OkBuffer result = new OkBuffer();
- if (size() == 0) return result;
-
- result.write(head.data, head.pos, head.limit - head.pos);
- for (Segment s = head.next; s != head; s = s.next) {
- result.write(s.data, s.pos, s.limit - s.pos);
- }
-
- return result;
- }
-}
diff --git a/okio/src/main/java/okio/Okio.java b/okio/src/main/java/okio/Okio.java
deleted file mode 100644
index 3a9b4f9..0000000
--- a/okio/src/main/java/okio/Okio.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import static okio.Util.checkOffsetAndCount;
-
-public final class Okio {
- private Okio() {
- }
-
- public static BufferedSource buffer(Source source) {
- return new RealBufferedSource(source);
- }
-
- public static BufferedSink buffer(Sink sink) {
- return new RealBufferedSink(sink);
- }
-
- /** Copies bytes from {@code source} to {@code sink}. */
- public static void copy(OkBuffer source, long offset, long byteCount, OutputStream sink)
- throws IOException {
- checkOffsetAndCount(source.size, offset, byteCount);
-
- // Skip segments that we aren't copying from.
- Segment s = source.head;
- while (offset >= (s.limit - s.pos)) {
- offset -= (s.limit - s.pos);
- s = s.next;
- }
-
- // Copy from one segment at a time.
- while (byteCount > 0) {
- int pos = (int) (s.pos + offset);
- int toWrite = (int) Math.min(s.limit - pos, byteCount);
- sink.write(s.data, pos, toWrite);
- byteCount -= toWrite;
- offset = 0;
- }
- }
-
- /** Returns a sink that writes to {@code out}. */
- public static Sink sink(final OutputStream out) {
- return new Sink() {
- private Deadline deadline = Deadline.NONE;
-
- @Override public void write(OkBuffer source, long byteCount)
- throws IOException {
- checkOffsetAndCount(source.size, 0, byteCount);
- while (byteCount > 0) {
- deadline.throwIfReached();
- Segment head = source.head;
- int toCopy = (int) Math.min(byteCount, head.limit - head.pos);
- out.write(head.data, head.pos, toCopy);
-
- head.pos += toCopy;
- byteCount -= toCopy;
- source.size -= toCopy;
-
- if (head.pos == head.limit) {
- source.head = head.pop();
- SegmentPool.INSTANCE.recycle(head);
- }
- }
- }
-
- @Override public void flush() throws IOException {
- out.flush();
- }
-
- @Override public void close() throws IOException {
- out.close();
- }
-
- @Override public Sink deadline(Deadline deadline) {
- if (deadline == null) throw new IllegalArgumentException("deadline == null");
- this.deadline = deadline;
- return this;
- }
-
- @Override public String toString() {
- return "sink(" + out + ")";
- }
- };
- }
-
- /** Returns a source that reads from {@code in}. */
- public static Source source(final InputStream in) {
- return new Source() {
- private Deadline deadline = Deadline.NONE;
-
- @Override public long read(OkBuffer sink, long byteCount) throws IOException {
- if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
- deadline.throwIfReached();
- Segment tail = sink.writableSegment(1);
- int maxToCopy = (int) Math.min(byteCount, Segment.SIZE - tail.limit);
- int bytesRead = in.read(tail.data, tail.limit, maxToCopy);
- if (bytesRead == -1) return -1;
- tail.limit += bytesRead;
- sink.size += bytesRead;
- return bytesRead;
- }
-
- @Override public void close() throws IOException {
- in.close();
- }
-
- @Override public Source deadline(Deadline deadline) {
- if (deadline == null) throw new IllegalArgumentException("deadline == null");
- this.deadline = deadline;
- return this;
- }
-
- @Override public String toString() {
- return "source(" + in + ")";
- }
- };
- }
-}
diff --git a/okio/src/main/java/okio/RealBufferedSource.java b/okio/src/main/java/okio/RealBufferedSource.java
deleted file mode 100644
index 0189d0f..0000000
--- a/okio/src/main/java/okio/RealBufferedSource.java
+++ /dev/null
@@ -1,205 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-
-import static okio.Util.checkOffsetAndCount;
-
-final class RealBufferedSource implements BufferedSource {
- public final OkBuffer buffer;
- public final Source source;
- private boolean closed;
-
- public RealBufferedSource(Source source, OkBuffer buffer) {
- if (source == null) throw new IllegalArgumentException("source == null");
- this.buffer = buffer;
- this.source = source;
- }
-
- public RealBufferedSource(Source source) {
- this(source, new OkBuffer());
- }
-
- @Override public OkBuffer buffer() {
- return buffer;
- }
-
- @Override public long read(OkBuffer sink, long byteCount) throws IOException {
- if (byteCount < 0) throw new IllegalArgumentException("byteCount < 0: " + byteCount);
- if (closed) throw new IllegalStateException("closed");
-
- if (buffer.size == 0) {
- long read = source.read(buffer, Segment.SIZE);
- if (read == -1) return -1;
- }
-
- long toRead = Math.min(byteCount, buffer.size);
- return buffer.read(sink, toRead);
- }
-
- @Override public boolean exhausted() throws IOException {
- if (closed) throw new IllegalStateException("closed");
- return buffer.exhausted() && source.read(buffer, Segment.SIZE) == -1;
- }
-
- @Override public void require(long byteCount) throws IOException {
- if (closed) throw new IllegalStateException("closed");
- while (buffer.size < byteCount) {
- if (source.read(buffer, Segment.SIZE) == -1) throw new EOFException();
- }
- }
-
- @Override public byte readByte() throws IOException {
- require(1);
- return buffer.readByte();
- }
-
- @Override public ByteString readByteString(long byteCount) throws IOException {
- require(byteCount);
- return buffer.readByteString(byteCount);
- }
-
- @Override public String readUtf8(long byteCount) throws IOException {
- require(byteCount);
- return buffer.readUtf8(byteCount);
- }
-
- @Override public String readUtf8Line() throws IOException {
- long newline = indexOf((byte) '\n');
-
- if (newline == -1) {
- return buffer.size != 0 ? readUtf8(buffer.size) : null;
- }
-
- return buffer.readUtf8Line(newline);
- }
-
- @Override public String readUtf8LineStrict() throws IOException {
- long newline = indexOf((byte) '\n');
- if (newline == -1L) throw new EOFException();
- return buffer.readUtf8Line(newline);
- }
-
- @Override public short readShort() throws IOException {
- require(2);
- return buffer.readShort();
- }
-
- @Override public short readShortLe() throws IOException {
- require(2);
- return buffer.readShortLe();
- }
-
- @Override public int readInt() throws IOException {
- require(4);
- return buffer.readInt();
- }
-
- @Override public int readIntLe() throws IOException {
- require(4);
- return buffer.readIntLe();
- }
-
- @Override public long readLong() throws IOException {
- require(8);
- return buffer.readLong();
- }
-
- @Override public long readLongLe() throws IOException {
- require(8);
- return buffer.readLongLe();
- }
-
- @Override public void skip(long byteCount) throws IOException {
- if (closed) throw new IllegalStateException("closed");
- while (byteCount > 0) {
- if (buffer.size == 0 && source.read(buffer, Segment.SIZE) == -1) {
- throw new EOFException();
- }
- long toSkip = Math.min(byteCount, buffer.size());
- buffer.skip(toSkip);
- byteCount -= toSkip;
- }
- }
-
- @Override public long indexOf(byte b) throws IOException {
- if (closed) throw new IllegalStateException("closed");
- long start = 0;
- long index;
- while ((index = buffer.indexOf(b, start)) == -1) {
- start = buffer.size;
- if (source.read(buffer, Segment.SIZE) == -1) return -1L;
- }
- return index;
- }
-
- @Override public InputStream inputStream() {
- return new InputStream() {
- @Override public int read() throws IOException {
- if (closed) throw new IOException("closed");
- if (buffer.size == 0) {
- long count = source.read(buffer, Segment.SIZE);
- if (count == -1) return -1;
- }
- return buffer.readByte() & 0xff;
- }
-
- @Override public int read(byte[] data, int offset, int byteCount) throws IOException {
- if (closed) throw new IOException("closed");
- checkOffsetAndCount(data.length, offset, byteCount);
-
- if (buffer.size == 0) {
- long count = source.read(buffer, Segment.SIZE);
- if (count == -1) return -1;
- }
-
- return buffer.read(data, offset, byteCount);
- }
-
- @Override public int available() throws IOException {
- if (closed) throw new IOException("closed");
- return (int) Math.min(buffer.size, Integer.MAX_VALUE);
- }
-
- @Override public void close() throws IOException {
- RealBufferedSource.this.close();
- }
-
- @Override public String toString() {
- return RealBufferedSource.this + ".inputStream()";
- }
- };
- }
-
- @Override public Source deadline(Deadline deadline) {
- source.deadline(deadline);
- return this;
- }
-
- @Override public void close() throws IOException {
- if (closed) return;
- closed = true;
- source.close();
- buffer.clear();
- }
-
- @Override public String toString() {
- return "buffer(" + source + ")";
- }
-}
diff --git a/okio/src/test/java/okio/OkBufferTest.java b/okio/src/test/java/okio/OkBufferTest.java
deleted file mode 100644
index f69613a..0000000
--- a/okio/src/test/java/okio/OkBufferTest.java
+++ /dev/null
@@ -1,682 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Random;
-import org.junit.Test;
-
-import static java.util.Arrays.asList;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public final class OkBufferTest {
- @Test public void readAndWriteUtf8() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8("ab");
- assertEquals(2, buffer.size());
- buffer.writeUtf8("cdef");
- assertEquals(6, buffer.size());
- assertEquals("abcd", buffer.readUtf8(4));
- assertEquals(2, buffer.size());
- assertEquals("ef", buffer.readUtf8(2));
- assertEquals(0, buffer.size());
- try {
- buffer.readUtf8(1);
- fail();
- } catch (ArrayIndexOutOfBoundsException expected) {
- }
- }
-
- @Test public void completeSegmentByteCountOnEmptyBuffer() throws Exception {
- OkBuffer buffer = new OkBuffer();
- assertEquals(0, buffer.completeSegmentByteCount());
- }
-
- @Test public void completeSegmentByteCountOnBufferWithFullSegments() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8(repeat('a', Segment.SIZE * 4));
- assertEquals(Segment.SIZE * 4, buffer.completeSegmentByteCount());
- }
-
- @Test public void completeSegmentByteCountOnBufferWithIncompleteTailSegment() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8(repeat('a', Segment.SIZE * 4 - 10));
- assertEquals(Segment.SIZE * 3, buffer.completeSegmentByteCount());
- }
-
- @Test public void readUtf8SpansSegments() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8(repeat('a', Segment.SIZE * 2));
- buffer.readUtf8(Segment.SIZE - 1);
- assertEquals("aa", buffer.readUtf8(2));
- }
-
- @Test public void readUtf8EntireBuffer() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8(repeat('a', Segment.SIZE));
- assertEquals(repeat('a', Segment.SIZE), buffer.readUtf8(Segment.SIZE));
- }
-
- @Test public void toStringOnEmptyBuffer() throws Exception {
- OkBuffer buffer = new OkBuffer();
- assertEquals("OkBuffer[size=0]", buffer.toString());
- }
-
- @Test public void toStringOnSmallBufferIncludesContents() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.write(ByteString.decodeHex("a1b2c3d4e5f61a2b3c4d5e6f10203040"));
- assertEquals("OkBuffer[size=16 data=a1b2c3d4e5f61a2b3c4d5e6f10203040]", buffer.toString());
- }
-
- @Test public void toStringOnLargeBufferIncludesMd5() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.write(ByteString.encodeUtf8("12345678901234567"));
- assertEquals("OkBuffer[size=17 md5=2c9728a2138b2f25e9f89f99bdccf8db]", buffer.toString());
- }
-
- @Test public void toStringOnMultipleSegmentBuffer() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8(repeat('a', 6144));
- assertEquals("OkBuffer[size=6144 md5=d890021f28522533c1cc1b9b1f83ce73]", buffer.toString());
- }
-
- @Test public void multipleSegmentBuffers() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8(repeat('a', 1000));
- buffer.writeUtf8(repeat('b', 2500));
- buffer.writeUtf8(repeat('c', 5000));
- buffer.writeUtf8(repeat('d', 10000));
- buffer.writeUtf8(repeat('e', 25000));
- buffer.writeUtf8(repeat('f', 50000));
-
- assertEquals(repeat('a', 999), buffer.readUtf8(999)); // a...a
- assertEquals("a" + repeat('b', 2500) + "c", buffer.readUtf8(2502)); // ab...bc
- assertEquals(repeat('c', 4998), buffer.readUtf8(4998)); // c...c
- assertEquals("c" + repeat('d', 10000) + "e", buffer.readUtf8(10002)); // cd...de
- assertEquals(repeat('e', 24998), buffer.readUtf8(24998)); // e...e
- assertEquals("e" + repeat('f', 50000), buffer.readUtf8(50001)); // ef...f
- assertEquals(0, buffer.size());
- }
-
- @Test public void fillAndDrainPool() throws Exception {
- OkBuffer buffer = new OkBuffer();
-
- // Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it.
- buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
- buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
- assertEquals(0, SegmentPool.INSTANCE.byteCount);
-
- // Recycle MAX_SIZE segments. They're all in the pool.
- buffer.readByteString(SegmentPool.MAX_SIZE);
- assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
-
- // Recycle MAX_SIZE more segments. The pool is full so they get garbage collected.
- buffer.readByteString(SegmentPool.MAX_SIZE);
- assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
-
- // Take MAX_SIZE segments to drain the pool.
- buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
- assertEquals(0, SegmentPool.INSTANCE.byteCount);
-
- // Take MAX_SIZE more segments. The pool is drained so these will need to be allocated.
- buffer.write(new byte[(int) SegmentPool.MAX_SIZE]);
- assertEquals(0, SegmentPool.INSTANCE.byteCount);
- }
-
- @Test public void moveBytesBetweenBuffersShareSegment() throws Exception {
- int size = (Segment.SIZE / 2) - 1;
- List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
- assertEquals(asList(size * 2), segmentSizes);
- }
-
- @Test public void moveBytesBetweenBuffersReassignSegment() throws Exception {
- int size = (Segment.SIZE / 2) + 1;
- List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
- assertEquals(asList(size, size), segmentSizes);
- }
-
- @Test public void moveBytesBetweenBuffersMultipleSegments() throws Exception {
- int size = 3 * Segment.SIZE + 1;
- List<Integer> segmentSizes = moveBytesBetweenBuffers(repeat('a', size), repeat('b', size));
- assertEquals(asList(Segment.SIZE, Segment.SIZE, Segment.SIZE, 1,
- Segment.SIZE, Segment.SIZE, Segment.SIZE, 1), segmentSizes);
- }
-
- private List<Integer> moveBytesBetweenBuffers(String... contents) {
- StringBuilder expected = new StringBuilder();
- OkBuffer buffer = new OkBuffer();
- for (String s : contents) {
- OkBuffer source = new OkBuffer();
- source.writeUtf8(s);
- buffer.write(source, source.size());
- expected.append(s);
- }
- List<Integer> segmentSizes = buffer.segmentSizes();
- assertEquals(expected.toString(), buffer.readUtf8(expected.length()));
- return segmentSizes;
- }
-
- /** The big part of source's first segment is being moved. */
- @Test public void writeSplitSourceBufferLeft() throws Exception {
- int writeSize = Segment.SIZE / 2 + 1;
-
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('b', Segment.SIZE - 10));
-
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('a', Segment.SIZE * 2));
- sink.write(source, writeSize);
-
- assertEquals(asList(Segment.SIZE - 10, writeSize), sink.segmentSizes());
- assertEquals(asList(Segment.SIZE - writeSize, Segment.SIZE), source.segmentSizes());
- }
-
- /** The big part of source's first segment is staying put. */
- @Test public void writeSplitSourceBufferRight() throws Exception {
- int writeSize = Segment.SIZE / 2 - 1;
-
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('b', Segment.SIZE - 10));
-
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('a', Segment.SIZE * 2));
- sink.write(source, writeSize);
-
- assertEquals(asList(Segment.SIZE - 10, writeSize), sink.segmentSizes());
- assertEquals(asList(Segment.SIZE - writeSize, Segment.SIZE), source.segmentSizes());
- }
-
- @Test public void writePrefixDoesntSplit() throws Exception {
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('b', 10));
-
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('a', Segment.SIZE * 2));
- sink.write(source, 20);
-
- assertEquals(asList(30), sink.segmentSizes());
- assertEquals(asList(Segment.SIZE - 20, Segment.SIZE), source.segmentSizes());
- assertEquals(30, sink.size());
- assertEquals(Segment.SIZE * 2 - 20, source.size());
- }
-
- @Test public void writePrefixDoesntSplitButRequiresCompact() throws Exception {
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('b', Segment.SIZE - 10)); // limit = size - 10
- sink.readUtf8(Segment.SIZE - 20); // pos = size = 20
-
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('a', Segment.SIZE * 2));
- sink.write(source, 20);
-
- assertEquals(asList(30), sink.segmentSizes());
- assertEquals(asList(Segment.SIZE - 20, Segment.SIZE), source.segmentSizes());
- assertEquals(30, sink.size());
- assertEquals(Segment.SIZE * 2 - 20, source.size());
- }
-
- @Test public void readExhaustedSource() throws Exception {
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('a', 10));
-
- OkBuffer source = new OkBuffer();
-
- assertEquals(-1, source.read(sink, 10));
- assertEquals(10, sink.size());
- assertEquals(0, source.size());
- }
-
- @Test public void readZeroBytesFromSource() throws Exception {
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('a', 10));
-
- OkBuffer source = new OkBuffer();
-
- // Either 0 or -1 is reasonable here. For consistency with Android's
- // ByteArrayInputStream we return 0.
- assertEquals(-1, source.read(sink, 0));
- assertEquals(10, sink.size());
- assertEquals(0, source.size());
- }
-
- @Test public void moveAllRequestedBytesWithRead() throws Exception {
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('a', 10));
-
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('b', 15));
-
- assertEquals(10, source.read(sink, 10));
- assertEquals(20, sink.size());
- assertEquals(5, source.size());
- assertEquals(repeat('a', 10) + repeat('b', 10), sink.readUtf8(20));
- }
-
- @Test public void moveFewerThanRequestedBytesWithRead() throws Exception {
- OkBuffer sink = new OkBuffer();
- sink.writeUtf8(repeat('a', 10));
-
- OkBuffer source = new OkBuffer();
- source.writeUtf8(repeat('b', 20));
-
- assertEquals(20, source.read(sink, 25));
- assertEquals(30, sink.size());
- assertEquals(0, source.size());
- assertEquals(repeat('a', 10) + repeat('b', 20), sink.readUtf8(30));
- }
-
- @Test public void indexOf() throws Exception {
- OkBuffer buffer = new OkBuffer();
-
- // The segment is empty.
- assertEquals(-1, buffer.indexOf((byte) 'a'));
-
- // The segment has one value.
- buffer.writeUtf8("a"); // a
- assertEquals(0, buffer.indexOf((byte) 'a'));
- assertEquals(-1, buffer.indexOf((byte) 'b'));
-
- // The segment has lots of data.
- buffer.writeUtf8(repeat('b', Segment.SIZE - 2)); // ab...b
- assertEquals(0, buffer.indexOf((byte) 'a'));
- assertEquals(1, buffer.indexOf((byte) 'b'));
- assertEquals(-1, buffer.indexOf((byte) 'c'));
-
- // The segment doesn't start at 0, it starts at 2.
- buffer.readUtf8(2); // b...b
- assertEquals(-1, buffer.indexOf((byte) 'a'));
- assertEquals(0, buffer.indexOf((byte) 'b'));
- assertEquals(-1, buffer.indexOf((byte) 'c'));
-
- // The segment is full.
- buffer.writeUtf8("c"); // b...bc
- assertEquals(-1, buffer.indexOf((byte) 'a'));
- assertEquals(0, buffer.indexOf((byte) 'b'));
- assertEquals(Segment.SIZE - 3, buffer.indexOf((byte) 'c'));
-
- // The segment doesn't start at 2, it starts at 4.
- buffer.readUtf8(2); // b...bc
- assertEquals(-1, buffer.indexOf((byte) 'a'));
- assertEquals(0, buffer.indexOf((byte) 'b'));
- assertEquals(Segment.SIZE - 5, buffer.indexOf((byte) 'c'));
-
- // Two segments.
- buffer.writeUtf8("d"); // b...bcd, d is in the 2nd segment.
- assertEquals(asList(Segment.SIZE - 4, 1), buffer.segmentSizes());
- assertEquals(Segment.SIZE - 4, buffer.indexOf((byte) 'd'));
- assertEquals(-1, buffer.indexOf((byte) 'e'));
- }
-
- @Test public void indexOfWithOffset() throws Exception {
- OkBuffer buffer = new OkBuffer();
- int halfSegment = Segment.SIZE / 2;
- buffer.writeUtf8(repeat('a', halfSegment));
- buffer.writeUtf8(repeat('b', halfSegment));
- buffer.writeUtf8(repeat('c', halfSegment));
- buffer.writeUtf8(repeat('d', halfSegment));
- assertEquals(0, buffer.indexOf((byte) 'a', 0));
- assertEquals(halfSegment - 1, buffer.indexOf((byte) 'a', halfSegment - 1));
- assertEquals(halfSegment, buffer.indexOf((byte) 'b', halfSegment - 1));
- assertEquals(halfSegment * 2, buffer.indexOf((byte) 'c', halfSegment - 1));
- assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment - 1));
- assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 2));
- assertEquals(halfSegment * 3, buffer.indexOf((byte) 'd', halfSegment * 3));
- assertEquals(halfSegment * 4 - 1, buffer.indexOf((byte) 'd', halfSegment * 4 - 1));
- }
-
- @Test public void writeBytes() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeByte(0xab);
- data.writeByte(0xcd);
- assertEquals("OkBuffer[size=2 data=abcd]", data.toString());
- }
-
- @Test public void writeLastByteInSegment() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8(repeat('a', Segment.SIZE - 1));
- data.writeByte(0x20);
- data.writeByte(0x21);
- assertEquals(asList(Segment.SIZE, 1), data.segmentSizes());
- assertEquals(repeat('a', Segment.SIZE - 1), data.readUtf8(Segment.SIZE - 1));
- assertEquals("OkBuffer[size=2 data=2021]", data.toString());
- }
-
- @Test public void writeShort() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeShort(0xabcd);
- data.writeShort(0x4321);
- assertEquals("OkBuffer[size=4 data=abcd4321]", data.toString());
- }
-
- @Test public void writeShortLe() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeShortLe(0xabcd);
- data.writeShortLe(0x4321);
- assertEquals("OkBuffer[size=4 data=cdab2143]", data.toString());
- }
-
- @Test public void writeInt() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeInt(0xabcdef01);
- data.writeInt(0x87654321);
- assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
- }
-
- @Test public void writeLastIntegerInSegment() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8(repeat('a', Segment.SIZE - 4));
- data.writeInt(0xabcdef01);
- data.writeInt(0x87654321);
- assertEquals(asList(Segment.SIZE, 4), data.segmentSizes());
- assertEquals(repeat('a', Segment.SIZE - 4), data.readUtf8(Segment.SIZE - 4));
- assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
- }
-
- @Test public void writeIntegerDoesntQuiteFitInSegment() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8(repeat('a', Segment.SIZE - 3));
- data.writeInt(0xabcdef01);
- data.writeInt(0x87654321);
- assertEquals(asList(Segment.SIZE - 3, 8), data.segmentSizes());
- assertEquals(repeat('a', Segment.SIZE - 3), data.readUtf8(Segment.SIZE - 3));
- assertEquals("OkBuffer[size=8 data=abcdef0187654321]", data.toString());
- }
-
- @Test public void writeIntLe() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeIntLe(0xabcdef01);
- data.writeIntLe(0x87654321);
- assertEquals("OkBuffer[size=8 data=01efcdab21436587]", data.toString());
- }
-
- @Test public void writeLong() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeLong(0xabcdef0187654321L);
- data.writeLong(0xcafebabeb0b15c00L);
- assertEquals("OkBuffer[size=16 data=abcdef0187654321cafebabeb0b15c00]", data.toString());
- }
-
- @Test public void writeLongLe() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeLongLe(0xabcdef0187654321L);
- data.writeLongLe(0xcafebabeb0b15c00L);
- assertEquals("OkBuffer[size=16 data=2143658701efcdab005cb1b0bebafeca]", data.toString());
- }
-
- @Test public void readByte() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
- assertEquals(0xab, data.readByte() & 0xff);
- assertEquals(0xcd, data.readByte() & 0xff);
- assertEquals(0, data.size());
- }
-
- @Test public void readShort() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
- });
- assertEquals((short) 0xabcd, data.readShort());
- assertEquals((short) 0xef01, data.readShort());
- assertEquals(0, data.size());
- }
-
- @Test public void readShortLe() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10
- });
- assertEquals((short) 0xcdab, data.readShortLe());
- assertEquals((short) 0x10ef, data.readShortLe());
- assertEquals(0, data.size());
- }
-
- @Test public void readShortSplitAcrossMultipleSegments() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8(repeat('a', Segment.SIZE - 1));
- data.write(new byte[] { (byte) 0xab, (byte) 0xcd });
- data.readUtf8(Segment.SIZE - 1);
- assertEquals((short) 0xabcd, data.readShort());
- assertEquals(0, data.size());
- }
-
- @Test public void readInt() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01,
- (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21
- });
- assertEquals(0xabcdef01, data.readInt());
- assertEquals(0x87654321, data.readInt());
- assertEquals(0, data.size());
- }
-
- @Test public void readIntLe() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
- (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21
- });
- assertEquals(0x10efcdab, data.readIntLe());
- assertEquals(0x21436587, data.readIntLe());
- assertEquals(0, data.size());
- }
-
- @Test public void readIntSplitAcrossMultipleSegments() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8(repeat('a', Segment.SIZE - 3));
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01
- });
- data.readUtf8(Segment.SIZE - 3);
- assertEquals(0xabcdef01, data.readInt());
- assertEquals(0, data.size());
- }
-
- @Test public void readLong() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
- (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
- (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69,
- (byte) 0x12, (byte) 0x23, (byte) 0x34, (byte) 0x45
- });
- assertEquals(0xabcdef1087654321L, data.readLong());
- assertEquals(0x3647586912233445L, data.readLong());
- assertEquals(0, data.size());
- }
-
- @Test public void readLongLe() throws Exception {
- OkBuffer data = new OkBuffer();
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x10,
- (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
- (byte) 0x36, (byte) 0x47, (byte) 0x58, (byte) 0x69,
- (byte) 0x12, (byte) 0x23, (byte) 0x34, (byte) 0x45
- });
- assertEquals(0x2143658710efcdabL, data.readLongLe());
- assertEquals(0x4534231269584736L, data.readLongLe());
- assertEquals(0, data.size());
- }
-
- @Test public void readLongSplitAcrossMultipleSegments() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8(repeat('a', Segment.SIZE - 7));
- data.write(new byte[] {
- (byte) 0xab, (byte) 0xcd, (byte) 0xef, (byte) 0x01,
- (byte) 0x87, (byte) 0x65, (byte) 0x43, (byte) 0x21,
- });
- data.readUtf8(Segment.SIZE - 7);
- assertEquals(0xabcdef0187654321L, data.readLong());
- assertEquals(0, data.size());
- }
-
- @Test public void byteAt() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8("a");
- buffer.writeUtf8(repeat('b', Segment.SIZE));
- buffer.writeUtf8("c");
- assertEquals('a', buffer.getByte(0));
- assertEquals('a', buffer.getByte(0)); // getByte doesn't mutate!
- assertEquals('c', buffer.getByte(buffer.size - 1));
- assertEquals('b', buffer.getByte(buffer.size - 2));
- assertEquals('b', buffer.getByte(buffer.size - 3));
- }
-
- @Test public void getByteOfEmptyBuffer() throws Exception {
- OkBuffer buffer = new OkBuffer();
- try {
- buffer.getByte(0);
- fail();
- } catch (IndexOutOfBoundsException expected) {
- }
- }
-
- @Test public void skip() throws Exception {
- OkBuffer buffer = new OkBuffer();
- buffer.writeUtf8("a");
- buffer.writeUtf8(repeat('b', Segment.SIZE));
- buffer.writeUtf8("c");
- buffer.skip(1);
- assertEquals('b', buffer.readByte() & 0xff);
- buffer.skip(Segment.SIZE - 2);
- assertEquals('b', buffer.readByte() & 0xff);
- buffer.skip(1);
- assertEquals(0, buffer.size());
- }
-
- @Test public void testWritePrefixToEmptyBuffer() {
- OkBuffer sink = new OkBuffer();
- OkBuffer source = new OkBuffer();
- source.writeUtf8("abcd");
- sink.write(source, 2);
- assertEquals("ab", sink.readUtf8(2));
- }
-
- @Test public void cloneDoesNotObserveWritesToOriginal() throws Exception {
- OkBuffer original = new OkBuffer();
- OkBuffer clone = original.clone();
- original.writeUtf8("abc");
- assertEquals(0, clone.size());
- }
-
- @Test public void cloneDoesNotObserveReadsFromOriginal() throws Exception {
- OkBuffer original = new OkBuffer();
- original.writeUtf8("abc");
- OkBuffer clone = original.clone();
- assertEquals("abc", original.readUtf8(3));
- assertEquals(3, clone.size());
- assertEquals("ab", clone.readUtf8(2));
- }
-
- @Test public void originalDoesNotObserveWritesToClone() throws Exception {
- OkBuffer original = new OkBuffer();
- OkBuffer clone = original.clone();
- clone.writeUtf8("abc");
- assertEquals(0, original.size());
- }
-
- @Test public void originalDoesNotObserveReadsFromClone() throws Exception {
- OkBuffer original = new OkBuffer();
- original.writeUtf8("abc");
- OkBuffer clone = original.clone();
- assertEquals("abc", clone.readUtf8(3));
- assertEquals(3, original.size());
- assertEquals("ab", original.readUtf8(2));
- }
-
- @Test public void cloneMultipleSegments() throws Exception {
- OkBuffer original = new OkBuffer();
- original.writeUtf8(repeat('a', Segment.SIZE * 3));
- OkBuffer clone = original.clone();
- original.writeUtf8(repeat('b', Segment.SIZE * 3));
- clone.writeUtf8(repeat('c', Segment.SIZE * 3));
-
- assertEquals(repeat('a', Segment.SIZE * 3) + repeat('b', Segment.SIZE * 3),
- original.readUtf8(Segment.SIZE * 6));
- assertEquals(repeat('a', Segment.SIZE * 3) + repeat('c', Segment.SIZE * 3),
- clone.readUtf8(Segment.SIZE * 6));
- }
-
- @Test public void testEqualsAndHashCodeEmpty() throws Exception {
- OkBuffer a = new OkBuffer();
- OkBuffer b = new OkBuffer();
- assertTrue(a.equals(b));
- assertTrue(a.hashCode() == b.hashCode());
- }
-
- @Test public void testEqualsAndHashCode() throws Exception {
- OkBuffer a = new OkBuffer().writeUtf8("dog");
- OkBuffer b = new OkBuffer().writeUtf8("hotdog");
- assertFalse(a.equals(b));
- assertFalse(a.hashCode() == b.hashCode());
-
- b.readUtf8(3); // Leaves b containing 'dog'.
- assertTrue(a.equals(b));
- assertTrue(a.hashCode() == b.hashCode());
- }
-
- @Test public void testEqualsAndHashCodeSpanningSegments() throws Exception {
- byte[] data = new byte[1024 * 1024];
- Random dice = new Random(0);
- dice.nextBytes(data);
-
- OkBuffer a = bufferWithRandomSegmentLayout(dice, data);
- OkBuffer b = bufferWithRandomSegmentLayout(dice, data);
- assertTrue(a.equals(b));
- assertTrue(a.hashCode() == b.hashCode());
-
- data[data.length / 2]++; // Change a single byte.
- OkBuffer c = bufferWithRandomSegmentLayout(dice, data);
- assertFalse(a.equals(c));
- assertFalse(a.hashCode() == c.hashCode());
- }
-
- /**
- * Returns a new buffer containing the data in {@code data}, and a segment
- * layout determined by {@code dice}.
- */
- private OkBuffer bufferWithRandomSegmentLayout(Random dice, byte[] data) {
- OkBuffer result = new OkBuffer();
-
- // Writing to result directly will yield packed segments. Instead, write to
- // other buffers, then write those buffers to result.
- for (int pos = 0, byteCount; pos < data.length; pos += byteCount) {
- byteCount = (Segment.SIZE / 2) + dice.nextInt(Segment.SIZE / 2);
- if (byteCount > data.length - pos) byteCount = data.length - pos;
- int offset = dice.nextInt(Segment.SIZE - byteCount);
-
- OkBuffer segment = new OkBuffer();
- segment.write(new byte[offset]);
- segment.write(data, pos, byteCount);
- segment.skip(offset);
-
- result.write(segment, byteCount);
- }
-
- return result;
- }
-
- private String repeat(char c, int count) {
- char[] array = new char[count];
- Arrays.fill(array, c);
- return new String(array);
- }
-}
diff --git a/okio/src/test/java/okio/OkioTest.java b/okio/src/test/java/okio/OkioTest.java
deleted file mode 100644
index e56979f..0000000
--- a/okio/src/test/java/okio/OkioTest.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.InputStream;
-import java.util.Arrays;
-import org.junit.Test;
-
-import static okio.Util.UTF_8;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.fail;
-
-public final class OkioTest {
- @Test public void sinkFromOutputStream() throws Exception {
- OkBuffer data = new OkBuffer();
- data.writeUtf8("a");
- data.writeUtf8(repeat('b', 9998));
- data.writeUtf8("c");
-
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- Sink sink = Okio.sink(out);
- sink.write(data, 3);
- assertEquals("abb", out.toString("UTF-8"));
- sink.write(data, data.size());
- assertEquals("a" + repeat('b', 9998) + "c", out.toString("UTF-8"));
- }
-
- @Test public void sourceFromInputStream() throws Exception {
- InputStream in = new ByteArrayInputStream(
- ("a" + repeat('b', Segment.SIZE * 2) + "c").getBytes(UTF_8));
-
- // Source: ab...bc
- Source source = Okio.source(in);
- OkBuffer sink = new OkBuffer();
-
- // Source: b...bc. Sink: abb.
- assertEquals(3, source.read(sink, 3));
- assertEquals("abb", sink.readUtf8(3));
-
- // Source: b...bc. Sink: b...b.
- assertEquals(Segment.SIZE, source.read(sink, 20000));
- assertEquals(repeat('b', Segment.SIZE), sink.readUtf8(sink.size()));
-
- // Source: b...bc. Sink: b...bc.
- assertEquals(Segment.SIZE - 1, source.read(sink, 20000));
- assertEquals(repeat('b', Segment.SIZE - 2) + "c", sink.readUtf8(sink.size()));
-
- // Source and sink are empty.
- assertEquals(-1, source.read(sink, 1));
- }
-
- @Test public void sourceFromInputStreamBounds() throws Exception {
- Source source = Okio.source(new ByteArrayInputStream(new byte[100]));
- try {
- source.read(new OkBuffer(), -1);
- fail();
- } catch (IllegalArgumentException expected) {
- }
- }
-
- private String repeat(char c, int count) {
- char[] array = new char[count];
- Arrays.fill(array, c);
- return new String(array);
- }
-}
diff --git a/okio/src/test/java/okio/ReadUtf8LineTest.java b/okio/src/test/java/okio/ReadUtf8LineTest.java
deleted file mode 100644
index 79b4c8a..0000000
--- a/okio/src/test/java/okio/ReadUtf8LineTest.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.EOFException;
-import java.io.IOException;
-import org.junit.Test;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-public abstract class ReadUtf8LineTest {
- protected abstract BufferedSource newSource(String s);
-
- @Test public void readLines() throws IOException {
- BufferedSource source = newSource("abc\ndef\n");
- assertEquals("abc", source.readUtf8LineStrict());
- assertEquals("def", source.readUtf8LineStrict());
- try {
- source.readUtf8LineStrict();
- fail();
- } catch (EOFException expected) {
- }
- }
-
- @Test public void emptyLines() throws IOException {
- BufferedSource source = newSource("\n\n\n");
- assertEquals("", source.readUtf8LineStrict());
- assertEquals("", source.readUtf8LineStrict());
- assertEquals("", source.readUtf8LineStrict());
- assertTrue(source.exhausted());
- }
-
- @Test public void crDroppedPrecedingLf() throws IOException {
- BufferedSource source = newSource("abc\r\ndef\r\nghi\rjkl\r\n");
- assertEquals("abc", source.readUtf8LineStrict());
- assertEquals("def", source.readUtf8LineStrict());
- assertEquals("ghi\rjkl", source.readUtf8LineStrict());
- }
-
- @Test public void bufferedReaderCompatible() throws IOException {
- BufferedSource source = newSource("abc\ndef");
- assertEquals("abc", source.readUtf8Line());
- assertEquals("def", source.readUtf8Line());
- assertEquals(null, source.readUtf8Line());
- }
-
- @Test public void bufferedReaderCompatibleWithTrailingNewline() throws IOException {
- BufferedSource source = newSource("abc\ndef\n");
- assertEquals("abc", source.readUtf8Line());
- assertEquals("def", source.readUtf8Line());
- assertEquals(null, source.readUtf8Line());
- }
-}
diff --git a/okio/src/test/java/okio/RealBufferedSourceReadUtf8LineTest.java b/okio/src/test/java/okio/RealBufferedSourceReadUtf8LineTest.java
deleted file mode 100644
index 8793640..0000000
--- a/okio/src/test/java/okio/RealBufferedSourceReadUtf8LineTest.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * Copyright (C) 2014 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 okio;
-
-import java.io.IOException;
-
-public final class RealBufferedSourceReadUtf8LineTest extends ReadUtf8LineTest {
- /** Returns a buffered source that gets bytes of {@code data} one at a time. */
- @Override protected BufferedSource newSource(String s) {
- final OkBuffer buffer = new OkBuffer().writeUtf8(s);
-
- Source slowSource = new Source() {
- @Override public long read(OkBuffer sink, long byteCount) throws IOException {
- return buffer.read(sink, Math.min(1, byteCount));
- }
-
- @Override public Source deadline(Deadline deadline) {
- throw new UnsupportedOperationException();
- }
-
- @Override public void close() throws IOException {
- throw new UnsupportedOperationException();
- }
- };
-
- return Okio.buffer(slowSource);
- }
-}
diff --git a/pom.xml b/pom.xml
index c534a1d..884718b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>OkHttp (Parent)</name>
@@ -22,8 +22,9 @@
<module>okhttp</module>
<module>okhttp-apache</module>
<module>okhttp-tests</module>
+ <module>okhttp-urlconnection</module>
+ <module>okhttp-android-support</module>
<module>okcurl</module>
- <module>okio</module>
<module>mockwebserver</module>
<module>samples</module>
<module>benchmarks</module>
@@ -33,9 +34,13 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Compilation -->
- <java.version>1.6</java.version>
- <npn.version>8.1.2.v20120308</npn.version>
- <bouncycastle.version>1.48</bouncycastle.version>
+ <java.version>1.7</java.version>
+ <okio.version>1.2.0</okio.version>
+ <!-- ALPN library targeted to Java 7 -->
+ <alpn.jdk7.version>7.1.2.v20141202</alpn.jdk7.version>
+ <!-- ALPN library targeted to Java 8 update 25. -->
+ <alpn.jdk8.version>8.1.2.v20141202</alpn.jdk8.version>
+ <bouncycastle.version>1.50</bouncycastle.version>
<gson.version>2.2.3</gson.version>
<apache.http.version>4.2.2</apache.http.version>
<airlift.version>0.6</airlift.version>
@@ -67,9 +72,9 @@
<dependencyManagement>
<dependencies>
<dependency>
- <groupId>org.mortbay.jetty.npn</groupId>
- <artifactId>npn-boot</artifactId>
- <version>${npn.version}</version>
+ <groupId>com.squareup.okio</groupId>
+ <artifactId>okio</artifactId>
+ <version>${okio.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
@@ -120,15 +125,12 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
- <version>2.16</version>
- <configuration>
- <argLine>-Xbootclasspath/p:${settings.localRepository}/org/mortbay/jetty/npn/npn-boot/${npn.version}/npn-boot-${npn.version}.jar</argLine>
- </configuration>
+ <version>2.17</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit47</artifactId>
- <version>2.16</version>
+ <version>2.17</version>
</dependency>
</dependencies>
</plugin>
@@ -166,6 +168,7 @@
<failsOnError>true</failsOnError>
<configLocation>checkstyle.xml</configLocation>
<consoleOutput>true</consoleOutput>
+ <excludes>**/CipherSuite.java</excludes>
</configuration>
<executions>
<execution>
@@ -179,7 +182,7 @@
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
- <version>1.10</version>
+ <version>1.11</version>
<executions>
<execution>
<phase>test</phase>
@@ -198,5 +201,66 @@
</plugin>
</plugins>
</build>
+
+ <profiles>
+ <profile>
+ <id>alpn-when-jdk7</id>
+ <activation>
+ <jdk>1.7</jdk>
+ </activation>
+ <properties>
+ <bootclasspathPrefix>${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/${alpn.jdk7.version}/alpn-boot-${alpn.jdk7.version}.jar</bootclasspathPrefix>
+ </properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>-Xbootclasspath/p:${bootclasspathPrefix}</argLine>
+ </configuration>
+ <dependencies>
+ <dependency>
+ <groupId>org.mortbay.jetty.alpn</groupId>
+ <artifactId>alpn-boot</artifactId>
+ <version>${alpn.jdk7.version}</version>
+ </dependency>
+ </dependencies>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </profile>
+ <profile>
+ <id>alpn-when-jdk8</id>
+ <activation>
+ <jdk>1.8</jdk>
+ </activation>
+ <properties>
+ <bootclasspathPrefix>${settings.localRepository}/org/mortbay/jetty/alpn/alpn-boot/${alpn.jdk8.version}/alpn-boot-${alpn.jdk8.version}.jar</bootclasspathPrefix>
+ </properties>
+ <build>
+ <pluginManagement>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <argLine>-Xbootclasspath/p:${bootclasspathPrefix}</argLine>
+ </configuration>
+ <dependencies>
+ <dependency>
+ <groupId>org.mortbay.jetty.alpn</groupId>
+ <artifactId>alpn-boot</artifactId>
+ <version>${alpn.jdk8.version}</version>
+ </dependency>
+ </dependencies>
+ </plugin>
+ </plugins>
+ </pluginManagement>
+ </build>
+ </profile>
+ </profiles>
</project>
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
index 0015f7c..2be2bab 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.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>crawler</artifactId>
diff --git a/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
index d80c13f..24383fe 100644
--- a/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
+++ b/samples/crawler/src/main/java/com/squareup/okhttp/sample/Crawler.java
@@ -15,17 +15,14 @@
*/
package com.squareup.okhttp.sample;
-import com.squareup.okhttp.HttpResponseCache;
-import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.Cache;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
import java.io.File;
import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
-import java.nio.charset.Charset;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -40,11 +37,9 @@
* Fetches HTML from a requested URL, follows the links, and repeats.
*/
public final class Crawler {
- public static final Charset UTF_8 = Charset.forName("UTF-8");
-
private final OkHttpClient client;
private final Set<URL> fetchedUrls = Collections.synchronizedSet(new LinkedHashSet<URL>());
- private final LinkedBlockingQueue<URL> queue = new LinkedBlockingQueue<URL>();
+ private final LinkedBlockingQueue<URL> queue = new LinkedBlockingQueue<>();
public Crawler(OkHttpClient client) {
this.client = client;
@@ -81,33 +76,29 @@
}
public void fetch(URL url) throws IOException {
- HttpURLConnection connection = client.open(url);
- String responseSource = connection.getHeaderField(OkHeaders.RESPONSE_SOURCE);
- String contentType = connection.getHeaderField("Content-Type");
- int responseCode = connection.getResponseCode();
+ Request request = new Request.Builder()
+ .url(url)
+ .build();
+ Response response = client.newCall(request).execute();
+ String responseSource = response.networkResponse() != null
+ ? ("(network: " + response.networkResponse().code() + ")")
+ : "(cache)";
+ int responseCode = response.code();
System.out.printf("%03d: %s %s%n", responseCode, url, responseSource);
- if (responseCode >= 400) {
- connection.getErrorStream().close();
- return;
- }
-
- InputStream in = connection.getInputStream();
+ String contentType = response.header("Content-Type");
if (responseCode != 200 || contentType == null) {
- in.close();
+ response.body().close();
return;
}
- MediaType mediaType = MediaType.parse(contentType);
- Document document = Jsoup.parse(in, mediaType.charset(UTF_8).name(), url.toString());
+ Document document = Jsoup.parse(response.body().string(), url.toString());
for (Element element : document.select("a[href]")) {
String href = element.attr("href");
URL link = parseUrl(url, href);
if (link != null) queue.add(link);
}
-
- in.close();
}
private URL parseUrl(URL url, String href) {
@@ -131,8 +122,8 @@
long cacheByteCount = 1024L * 1024L * 100L;
OkHttpClient client = new OkHttpClient();
- HttpResponseCache httpResponseCache = new HttpResponseCache(new File(args[0]), cacheByteCount);
- client.setOkResponseCache(httpResponseCache);
+ Cache cache = new Cache(new File(args[0]), cacheByteCount);
+ client.setCache(cache);
Crawler crawler = new Crawler(client);
crawler.queue.add(new URL(args[1]));
diff --git a/samples/guide/pom.xml b/samples/guide/pom.xml
index 61e5875..1cc1b6a 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.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>guide</artifactId>
@@ -18,5 +18,9 @@
<artifactId>okhttp</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.google.code.gson</groupId>
+ <artifactId>gson</artifactId>
+ </dependency>
</dependencies>
</project>
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/guide/GetExample.java b/samples/guide/src/main/java/com/squareup/okhttp/guide/GetExample.java
index b5427c5..aa2f200 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/guide/GetExample.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/guide/GetExample.java
@@ -1,43 +1,25 @@
package com.squareup.okhttp.guide;
import com.squareup.okhttp.OkHttpClient;
-import java.io.ByteArrayOutputStream;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
public class GetExample {
OkHttpClient client = new OkHttpClient();
- void run() throws IOException {
- String result = get(new URL("https://raw.github.com/square/okhttp/master/README.md"));
- System.out.println(result);
- }
+ String run(String url) throws IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .build();
- String get(URL url) throws IOException {
- HttpURLConnection connection = client.open(url);
- InputStream in = null;
- try {
- // Read the response.
- in = connection.getInputStream();
- byte[] response = readFully(in);
- return new String(response, "UTF-8");
- } finally {
- if (in != null) in.close();
- }
- }
-
- byte[] readFully(InputStream in) throws IOException {
- ByteArrayOutputStream out = new ByteArrayOutputStream();
- byte[] buffer = new byte[1024];
- for (int count; (count = in.read(buffer)) != -1; ) {
- out.write(buffer, 0, count);
- }
- return out.toByteArray();
+ Response response = client.newCall(request).execute();
+ return response.body().string();
}
public static void main(String[] args) throws IOException {
- new GetExample().run();
+ GetExample example = new GetExample();
+ String response = example.run("https://raw.github.com/square/okhttp/master/README.md");
+ System.out.println(response);
}
}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java b/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
index 309ab70..5de644c 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java
@@ -1,51 +1,26 @@
package com.squareup.okhttp.guide;
+import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.OkHttpClient;
-import java.io.BufferedReader;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
public class PostExample {
+ public static final MediaType JSON
+ = MediaType.parse("application/json; charset=utf-8");
+
OkHttpClient client = new OkHttpClient();
- void run() throws IOException {
- byte[] body = bowlingJson("Jesse", "Jake").getBytes("UTF-8");
- String result = post(new URL("http://www.roundsapp.com/post"), body);
- System.out.println(result);
- }
-
- String post(URL url, byte[] body) throws IOException {
- HttpURLConnection connection = client.open(url);
- OutputStream out = null;
- InputStream in = null;
- try {
- // Write the request.
- connection.setRequestMethod("POST");
- out = connection.getOutputStream();
- out.write(body);
- out.close();
-
- // Read the response.
- if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
- throw new IOException("Unexpected HTTP response: "
- + connection.getResponseCode() + " " + connection.getResponseMessage());
- }
- in = connection.getInputStream();
- return readFirstLine(in);
- } finally {
- // Clean up.
- if (out != null) out.close();
- if (in != null) in.close();
- }
- }
-
- String readFirstLine(InputStream in) throws IOException {
- BufferedReader reader = new BufferedReader(new InputStreamReader(in, "UTF-8"));
- return reader.readLine();
+ String post(String url, String json) throws IOException {
+ RequestBody body = RequestBody.create(JSON, json);
+ Request request = new Request.Builder()
+ .url(url)
+ .post(body)
+ .build();
+ Response response = client.newCall(request).execute();
+ return response.body().string();
}
String bowlingJson(String player1, String player2) {
@@ -61,6 +36,9 @@
}
public static void main(String[] args) throws IOException {
- new PostExample().run();
+ PostExample example = new PostExample();
+ String json = example.bowlingJson("Jesse", "Jake");
+ String response = example.post("http://www.roundsapp.com/post", json);
+ System.out.println(response);
}
}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/AccessHeaders.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/AccessHeaders.java
new file mode 100644
index 0000000..9fe9d1a
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/AccessHeaders.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+
+public final class AccessHeaders {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("https://api.github.com/repos/square/okhttp/issues")
+ .header("User-Agent", "OkHttp Headers.java")
+ .addHeader("Accept", "application/json; q=0.5")
+ .addHeader("Accept", "application/vnd.github.v3+json")
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println("Server: " + response.header("Server"));
+ System.out.println("Date: " + response.header("Date"));
+ System.out.println("Vary: " + response.headers("Vary"));
+ }
+
+ public static void main(String... args) throws Exception {
+ new AccessHeaders().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/AsynchronousGet.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/AsynchronousGet.java
new file mode 100644
index 0000000..34cfc58
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/AsynchronousGet.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Callback;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+
+public final class AsynchronousGet {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("http://publicobject.com/helloworld.txt")
+ .build();
+
+ client.newCall(request).enqueue(new Callback() {
+ @Override public void onFailure(Request request, IOException e) {
+ e.printStackTrace();
+ }
+
+ @Override public void onResponse(Response response) throws IOException {
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ Headers responseHeaders = response.headers();
+ for (int i = 0, size = responseHeaders.size(); i < size; i++) {
+ System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
+ }
+
+ System.out.println(response.body().string());
+ }
+ });
+ }
+
+ public static void main(String... args) throws Exception {
+ new AsynchronousGet().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/Authenticate.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/Authenticate.java
new file mode 100644
index 0000000..44581ae
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/Authenticate.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Authenticator;
+import com.squareup.okhttp.Credentials;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.net.Proxy;
+
+public final class Authenticate {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ client.setAuthenticator(new Authenticator() {
+ @Override public Request authenticate(Proxy proxy, Response response) {
+ System.out.println("Authenticating for response: " + response);
+ System.out.println("Challenges: " + response.challenges());
+ String credential = Credentials.basic("jesse", "password1");
+ return response.request().newBuilder()
+ .header("Authorization", credential)
+ .build();
+ }
+
+ @Override public Request authenticateProxy(Proxy proxy, Response response) {
+ return null; // Null indicates no attempt to authenticate.
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url("http://publicobject.com/secrets/hellosecret.txt")
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new Authenticate().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/CacheResponse.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CacheResponse.java
new file mode 100644
index 0000000..3335ebe
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CacheResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Cache;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.File;
+import java.io.IOException;
+
+public final class CacheResponse {
+ private final OkHttpClient client;
+
+ public CacheResponse(File cacheDirectory) throws Exception {
+ int cacheSize = 10 * 1024 * 1024; // 10 MiB
+ Cache cache = new Cache(cacheDirectory, cacheSize);
+
+ client = new OkHttpClient();
+ client.setCache(cache);
+ }
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("http://publicobject.com/helloworld.txt")
+ .build();
+
+ Response response1 = client.newCall(request).execute();
+ if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
+
+ String response1Body = response1.body().string();
+ System.out.println("Response 1 response: " + response1);
+ System.out.println("Response 1 cache response: " + response1.cacheResponse());
+ System.out.println("Response 1 network response: " + response1.networkResponse());
+
+ Response response2 = client.newCall(request).execute();
+ if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
+
+ String response2Body = response2.body().string();
+ System.out.println("Response 2 response: " + response2);
+ System.out.println("Response 2 cache response: " + response2.cacheResponse());
+ System.out.println("Response 2 network response: " + response2.networkResponse());
+
+ System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
+ }
+
+ public static void main(String... args) throws Exception {
+ new CacheResponse(new File("CacheResponse.tmp")).run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/CancelCall.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CancelCall.java
new file mode 100644
index 0000000..9f8d373
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CancelCall.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class CancelCall {
+ private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
+ .build();
+
+ final long startNanos = System.nanoTime();
+ final Call call = client.newCall(request);
+
+ // Schedule a job to cancel the call in 1 second.
+ executor.schedule(new Runnable() {
+ @Override public void run() {
+ System.out.printf("%.2f Canceling call.%n", (System.nanoTime() - startNanos) / 1e9f);
+ call.cancel();
+ System.out.printf("%.2f Canceled call.%n", (System.nanoTime() - startNanos) / 1e9f);
+ }
+ }, 1, TimeUnit.SECONDS);
+
+ try {
+ System.out.printf("%.2f Executing call.%n", (System.nanoTime() - startNanos) / 1e9f);
+ Response response = call.execute();
+ System.out.printf("%.2f Call was expected to fail, but completed: %s%n",
+ (System.nanoTime() - startNanos) / 1e9f, response);
+ } catch (IOException e) {
+ System.out.printf("%.2f Call failed as expected: %s%n",
+ (System.nanoTime() - startNanos) / 1e9f, e);
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new CancelCall().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/CertificatePinning.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CertificatePinning.java
new file mode 100644
index 0000000..b643d52
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CertificatePinning.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.security.cert.Certificate;
+
+public final class CertificatePinning {
+ private final OkHttpClient client;
+
+ public CertificatePinning() {
+ client = new OkHttpClient();
+ client.setCertificatePinner(
+ new CertificatePinner.Builder()
+ .add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
+ .add("publicobject.com", "sha1/SXxoaOSEzPC6BgGmxAt/EAcsajw=")
+ .add("publicobject.com", "sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=")
+ .add("publicobject.com", "sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=")
+ .build());
+ }
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("https://publicobject.com/robots.txt")
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ for (Certificate certificate : response.handshake().peerCertificates()) {
+ System.out.println(CertificatePinner.pin(certificate));
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new CertificatePinning().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/CheckHandshake.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CheckHandshake.java
new file mode 100644
index 0000000..0a2e86e
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/CheckHandshake.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.security.cert.Certificate;
+import java.util.Collections;
+import java.util.Set;
+
+public final class CheckHandshake {
+ /** Rejects otherwise-trusted certificates. */
+ private static final Interceptor CHECK_HANDSHAKE_INTERCEPTOR = new Interceptor() {
+ Set<String> blacklist = Collections.singleton("sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=");
+
+ @Override public Response intercept(Chain chain) throws IOException {
+ for (Certificate certificate : chain.connection().getHandshake().peerCertificates()) {
+ String pin = CertificatePinner.pin(certificate);
+ if (blacklist.contains(pin)) {
+ throw new IOException("Blacklisted peer certificate: " + pin);
+ }
+ }
+ return chain.proceed(chain.request());
+ }
+ };
+
+ private final OkHttpClient client = new OkHttpClient();
+
+ public CheckHandshake() {
+ client.networkInterceptors().add(CHECK_HANDSHAKE_INTERCEPTOR);
+ }
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("https://publicobject.com/helloworld.txt")
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new CheckHandshake().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/ConfigureTimeouts.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/ConfigureTimeouts.java
new file mode 100644
index 0000000..f358a45
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/ConfigureTimeouts.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.util.concurrent.TimeUnit;
+
+public final class ConfigureTimeouts {
+ private final OkHttpClient client;
+
+ public ConfigureTimeouts() throws Exception {
+ client = new OkHttpClient();
+ client.setConnectTimeout(10, TimeUnit.SECONDS);
+ client.setWriteTimeout(10, TimeUnit.SECONDS);
+ client.setReadTimeout(30, TimeUnit.SECONDS);
+ }
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("http://httpbin.org/delay/2") // This URL is served with a 2 second delay.
+ .build();
+
+ Response response = client.newCall(request).execute();
+ System.out.println("Response completed: " + response);
+ }
+
+ public static void main(String... args) throws Exception {
+ new ConfigureTimeouts().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java
new file mode 100644
index 0000000..d70f107
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/LoggingInterceptors.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.util.logging.Logger;
+
+public final class LoggingInterceptors {
+ private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
+ private final OkHttpClient client = new OkHttpClient();
+
+ public LoggingInterceptors() {
+ client.networkInterceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ long t1 = System.nanoTime();
+ Request request = chain.request();
+ logger.info(String.format("Sending request %s on %s%n%s",
+ request.url(), chain.connection(), request.headers()));
+ Response response = chain.proceed(request);
+
+ long t2 = System.nanoTime();
+ logger.info(String.format("Received response for %s in %.1fms%n%s",
+ request.url(), (t2 - t1) / 1e6d, response.headers()));
+ return response;
+ }
+ });
+ }
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("https://publicobject.com/helloworld.txt")
+ .build();
+
+ Response response = client.newCall(request).execute();
+ response.body().close();
+ }
+
+ public static void main(String... args) throws Exception {
+ new LoggingInterceptors().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/ParseResponseWithGson.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/ParseResponseWithGson.java
new file mode 100644
index 0000000..cf63f0d
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/ParseResponseWithGson.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.google.gson.Gson;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.util.Map;
+
+public final class ParseResponseWithGson {
+ private final OkHttpClient client = new OkHttpClient();
+ private final Gson gson = new Gson();
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("https://api.github.com/gists/c2a7c39532239ff261be")
+ .build();
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
+ for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
+ System.out.println(entry.getKey());
+ System.out.println(entry.getValue().content);
+ }
+ }
+
+ static class Gist {
+ Map<String, GistFile> files;
+ }
+
+ static class GistFile {
+ String content;
+ }
+
+ public static void main(String... args) throws Exception {
+ new ParseResponseWithGson().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PerCallSettings.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PerCallSettings.java
new file mode 100644
index 0000000..af4956e
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PerCallSettings.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+public final class PerCallSettings {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("http://httpbin.org/delay/1") // This URL is served with a 1 second delay.
+ .build();
+
+ try {
+ OkHttpClient cloned = client.clone(); // Clone to make a customized OkHttp for this request.
+ cloned.setReadTimeout(500, TimeUnit.MILLISECONDS);
+
+ Response response = cloned.newCall(request).execute();
+ System.out.println("Response 1 succeeded: " + response);
+ } catch (IOException e) {
+ System.out.println("Response 1 failed: " + e);
+ }
+
+ try {
+ OkHttpClient cloned = client.clone(); // Clone to make a customized OkHttp for this request.
+ cloned.setReadTimeout(3000, TimeUnit.MILLISECONDS);
+
+ Response response = cloned.newCall(request).execute();
+ System.out.println("Response 2 succeeded: " + response);
+ } catch (IOException e) {
+ System.out.println("Response 2 failed: " + e);
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new PerCallSettings().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostFile.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostFile.java
new file mode 100644
index 0000000..a0d98df
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostFile.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+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 java.io.File;
+import java.io.IOException;
+
+public final class PostFile {
+ public static final MediaType MEDIA_TYPE_MARKDOWN
+ = MediaType.parse("text/x-markdown; charset=utf-8");
+
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ File file = new File("README.md");
+
+ Request request = new Request.Builder()
+ .url("https://api.github.com/markdown/raw")
+ .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new PostFile().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostForm.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostForm.java
new file mode 100644
index 0000000..30054f1
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostForm.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.FormEncodingBuilder;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+
+public final class PostForm {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ RequestBody formBody = new FormEncodingBuilder()
+ .add("search", "Jurassic Park")
+ .build();
+ Request request = new Request.Builder()
+ .url("https://en.wikipedia.org/w/index.php")
+ .post(formBody)
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new PostForm().run();
+ }
+}
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
new file mode 100644
index 0000000..8e5334a
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.MultipartBuilder;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import java.io.File;
+import java.io.IOException;
+
+public final class PostMultipart {
+ /**
+ * The imgur client ID for OkHttp recipes. If you're using imgur for anything
+ * other than running these examples, please request your own client ID!
+ * https://api.imgur.com/oauth2
+ */
+ private static final String IMGUR_CLIENT_ID = "9199fdef135c122";
+ private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
+
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
+ RequestBody requestBody = new MultipartBuilder()
+ .type(MultipartBuilder.FORM)
+ .addFormDataPart("title", "Square Logo")
+ .addFormDataPart("image", null,
+ RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
+ .build();
+
+ Request request = new Request.Builder()
+ .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
+ .url("https://api.imgur.com/3/image")
+ .post(requestBody)
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new PostMultipart().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostStreaming.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostStreaming.java
new file mode 100644
index 0000000..500344c
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostStreaming.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+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 java.io.IOException;
+import okio.BufferedSink;
+
+public final class PostStreaming {
+ public static final MediaType MEDIA_TYPE_MARKDOWN
+ = MediaType.parse("text/x-markdown; charset=utf-8");
+
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ RequestBody requestBody = new RequestBody() {
+ @Override public MediaType contentType() {
+ return MEDIA_TYPE_MARKDOWN;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Numbers\n");
+ sink.writeUtf8("-------\n");
+ for (int i = 2; i <= 997; i++) {
+ sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
+ }
+ }
+
+ private String factor(int n) {
+ for (int i = 2; i < n; i++) {
+ int x = n / i;
+ if (x * i == n) return factor(x) + " × " + i;
+ }
+ return Integer.toString(n);
+ }
+ };
+
+ Request request = new Request.Builder()
+ .url("https://api.github.com/markdown/raw")
+ .post(requestBody)
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new PostStreaming().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostString.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostString.java
new file mode 100644
index 0000000..943636a
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostString.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+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 java.io.IOException;
+
+public final class PostString {
+ public static final MediaType MEDIA_TYPE_MARKDOWN
+ = MediaType.parse("text/x-markdown; charset=utf-8");
+
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ String postBody = ""
+ + "Releases\n"
+ + "--------\n"
+ + "\n"
+ + " * _1.0_ May 6, 2013\n"
+ + " * _1.1_ June 15, 2013\n"
+ + " * _1.2_ August 11, 2013\n";
+
+ Request request = new Request.Builder()
+ .url("https://api.github.com/markdown/raw")
+ .post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new PostString().run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/RequestBodyCompression.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/RequestBodyCompression.java
new file mode 100644
index 0000000..c4805bd
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/RequestBodyCompression.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.google.gson.Gson;
+import com.squareup.okhttp.Interceptor;
+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 java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import okio.BufferedSink;
+import okio.GzipSink;
+import okio.Okio;
+
+public final class RequestBodyCompression {
+ /**
+ * The Google API KEY for OkHttp recipes. If you're using Google APIs for anything other than
+ * running these examples, please request your own client ID!
+ * https://console.developers.google.com/project
+ */
+ public static final String GOOGLE_API_KEY = "AIzaSyAx2WZYe0My0i-uGurpvraYJxO7XNbwiGs";
+ public static final MediaType MEDIA_TYPE_JSON = MediaType.parse("application/json");
+
+ private final OkHttpClient client = new OkHttpClient();
+
+ public RequestBodyCompression() {
+ client.interceptors().add(new GzipRequestInterceptor());
+ }
+
+ public void run() throws Exception {
+ Map<String, String> requestBody = new LinkedHashMap<>();
+ requestBody.put("longUrl", "https://publicobject.com/2014/12/04/html-formatting-javadocs/");
+ RequestBody jsonRequestBody = RequestBody.create(
+ MEDIA_TYPE_JSON, new Gson().toJson(requestBody));
+ Request request = new Request.Builder()
+ .url("https://www.googleapis.com/urlshortener/v1/url?key=" + GOOGLE_API_KEY)
+ .post(jsonRequestBody)
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new RequestBodyCompression().run();
+ }
+
+ /** This interceptor compresses the HTTP request body. Many webservers can't handle this! */
+ static class GzipRequestInterceptor implements Interceptor {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ if (originalRequest.body() == null || originalRequest.header("Content-Encoding") != null) {
+ return chain.proceed(originalRequest);
+ }
+
+ Request compressedRequest = originalRequest.newBuilder()
+ .header("Content-Encoding", "gzip")
+ .method(originalRequest.method(), gzip(originalRequest.body()))
+ .build();
+ return chain.proceed(compressedRequest);
+ }
+
+ private RequestBody gzip(final RequestBody body) {
+ return new RequestBody() {
+ @Override public MediaType contentType() {
+ return body.contentType();
+ }
+
+ @Override public long contentLength() {
+ return -1; // We don't know the compressed length in advance!
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ BufferedSink gzipSink = Okio.buffer(new GzipSink(sink));
+ body.writeTo(gzipSink);
+ gzipSink.close();
+ }
+ };
+ }
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/RewriteResponseCacheControl.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/RewriteResponseCacheControl.java
new file mode 100644
index 0000000..63f819e
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/RewriteResponseCacheControl.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Cache;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.File;
+import java.io.IOException;
+
+public final class RewriteResponseCacheControl {
+ /** Dangerous interceptor that rewrites the server's cache-control header. */
+ private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ Response originalResponse = chain.proceed(chain.request());
+ return originalResponse.newBuilder()
+ .header("Cache-Control", "max-age=60")
+ .build();
+ }
+ };
+
+ private final OkHttpClient client;
+
+ public RewriteResponseCacheControl(File cacheDirectory) throws Exception {
+ Cache cache = new Cache(cacheDirectory, 1024 * 1024);
+ cache.evictAll();
+
+ client = new OkHttpClient();
+ client.setCache(cache);
+ }
+
+ public void run() throws Exception {
+ for (int i = 0; i < 5; i++) {
+ System.out.println(" Request: " + i);
+
+ Request request = new Request.Builder()
+ .url("https://api.github.com/search/repositories?q=http")
+ .build();
+
+ if (i == 2) {
+ // Force this request's response to be written to the cache. This way, subsequent responses
+ // can be read from the cache.
+ System.out.println("Force cache: true");
+ client.networkInterceptors().add(REWRITE_CACHE_CONTROL_INTERCEPTOR);
+ } else {
+ System.out.println("Force cache: false");
+ client.networkInterceptors().clear();
+ }
+
+ Response response = client.newCall(request).execute();
+ response.body().close();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ System.out.println(" Network: " + (response.networkResponse() != null));
+ System.out.println();
+ }
+ }
+
+ public static void main(String... args) throws Exception {
+ new RewriteResponseCacheControl(new File("RewriteResponseCacheControl.tmp")).run();
+ }
+}
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/SynchronousGet.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/SynchronousGet.java
new file mode 100644
index 0000000..6b4cecb
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/SynchronousGet.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2014 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.recipes;
+
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.IOException;
+
+public final class SynchronousGet {
+ private final OkHttpClient client = new OkHttpClient();
+
+ public void run() throws Exception {
+ Request request = new Request.Builder()
+ .url("https://publicobject.com/helloworld.txt")
+ .build();
+
+ Response response = client.newCall(request).execute();
+ if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
+
+ Headers responseHeaders = response.headers();
+ for (int i = 0; i < responseHeaders.size(); i++) {
+ System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
+ }
+
+ System.out.println(response.body().string());
+ }
+
+ public static void main(String... args) throws Exception {
+ new SynchronousGet().run();
+ }
+}
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
new file mode 100644
index 0000000..738191a
--- /dev/null
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
@@ -0,0 +1,70 @@
+package com.squareup.okhttp.recipes;
+
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.ws.WebSocket;
+import com.squareup.okhttp.internal.ws.WebSocketCall;
+import com.squareup.okhttp.internal.ws.WebSocketListener;
+import java.io.IOException;
+import okio.Buffer;
+import okio.BufferedSource;
+
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType;
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocket.PayloadType.TEXT;
+
+/**
+ * WARNING: This recipe is for an API that is not final and subject to change at any time!
+ */
+public final class WebSocketEcho implements WebSocketListener {
+ private void run() throws IOException {
+ OkHttpClient client = new OkHttpClient();
+
+ Request request = new Request.Builder()
+ .url("ws://echo.websocket.org")
+ .build();
+ WebSocketCall.newWebSocketCall(client, request).enqueue(this);
+
+ // Trigger shutdown of the dispatcher's executor so this process can exit cleanly.
+ client.getDispatcher().getExecutorService().shutdown();
+ }
+
+ @Override public void onOpen(WebSocket webSocket, Request request, Response response)
+ throws IOException {
+ webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello..."));
+ webSocket.sendMessage(TEXT, new Buffer().writeUtf8("...World!"));
+ webSocket.sendMessage(BINARY, new Buffer().writeInt(0xdeadbeef));
+ webSocket.close(1000, "Goodbye, World!");
+ }
+
+ @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
+ switch (type) {
+ case TEXT:
+ System.out.println(payload.readUtf8());
+ break;
+ case BINARY:
+ System.out.println(payload.readByteString().hex());
+ break;
+ default:
+ throw new IllegalStateException("Unknown payload type: " + type);
+ }
+ payload.close();
+ }
+
+ @Override public void onPong(Buffer payload) {
+ System.out.println("PONG: " + payload.readUtf8());
+ }
+
+ @Override public void onClose(int code, String reason) {
+ System.out.println("CLOSE: " + code + " " + reason);
+ }
+
+ @Override public void onFailure(IOException e) {
+ e.printStackTrace();
+ }
+
+ public static void main(String... args) throws IOException {
+ new WebSocketEcho().run();
+ }
+}
diff --git a/samples/pom.xml b/samples/pom.xml
index 62d1240..6991fa9 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<groupId>com.squareup.okhttp.sample</groupId>
diff --git a/samples/simple-client/pom.xml b/samples/simple-client/pom.xml
index 93657e2..3ebf058 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.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</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 c6424e2..e616d41 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
@@ -3,10 +3,9 @@
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.squareup.okhttp.OkHttpClient;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import java.io.Reader;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
@@ -27,12 +26,16 @@
OkHttpClient client = new OkHttpClient();
// Create request for remote resource.
- HttpURLConnection connection = client.open(new URL(ENDPOINT));
- InputStream is = connection.getInputStream();
- InputStreamReader isr = new InputStreamReader(is);
+ Request request = new Request.Builder()
+ .url(ENDPOINT)
+ .build();
+
+ // Execute the request and retrieve the response.
+ Response response = client.newCall(request).execute();
// Deserialize HTTP response to concrete type.
- List<Contributor> contributors = GSON.fromJson(isr, CONTRIBUTORS.getType());
+ Reader body = response.body().charStream();
+ List<Contributor> contributors = GSON.fromJson(body, CONTRIBUTORS.getType());
// 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 70188c7..e3dc358 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.0.0-SNAPSHOT</version>
+ <version>2.3.0-SNAPSHOT</version>
</parent>
<artifactId>static-server</artifactId>
diff --git a/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java b/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java
index cb0e24e..a2fd19d 100644
--- a/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java
+++ b/samples/static-server/src/main/java/com/squareup/okhttp/sample/SampleServer.java
@@ -16,6 +16,8 @@
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
+import okio.Buffer;
+import okio.Okio;
public class SampleServer extends Dispatcher {
private final SSLContext sslContext;
@@ -32,7 +34,7 @@
MockWebServer server = new MockWebServer();
server.useHttps(sslContext.getSocketFactory(), false);
server.setDispatcher(this);
- server.play(port);
+ server.start(port);
}
@Override public MockResponse dispatch(RecordedRequest request) {
@@ -82,9 +84,9 @@
.addHeader("content-type: " + contentType(path));
}
- private byte[] fileToBytes(File file) throws IOException {
- byte[] result = new byte[(int) file.length()];
- Util.readFully(new FileInputStream(file), result);
+ private Buffer fileToBytes(File file) throws IOException {
+ Buffer result = new Buffer();
+ result.writeAll(Okio.source(file));
return result;
}
diff --git a/website/index.html b/website/index.html
index 095102d..57c412f 100644
--- a/website/index.html
+++ b/website/index.html
@@ -43,12 +43,12 @@
<div class="row">
<div class="span9">
<h3 id="overview">Overview</h3>
- <p>HTTP is the way modern applications network. It’s how we exchange data & media.
+ <p>HTTP is the way modern applications network. It’s how we exchange data & media.
Doing HTTP efficiently makes your stuff load faster and saves bandwidth.</p>
<p>OkHttp is an HTTP client that’s efficient by default:</p>
<ul>
- <li>SPDY support allows all requests to the same host to share a socket.</li>
+ <li>HTTP/2 and SPDY support allows all requests to the same host to share a socket.</li>
<li>Connection pooling reduces request latency (if SPDY isn’t available).</li>
<li>Transparent GZIP shrinks download sizes.</li>
<li>Response caching avoids the network completely for repeat requests.</li>
@@ -57,78 +57,62 @@
<p>OkHttp perseveres when the network is troublesome: it will silently recover from
common connection problems. If your service has multiple IP addresses OkHttp will
attempt alternate addresses if the first connect fails. This is necessary for IPv4+IPv6
- and for services hosted in redundant data centers. OkHttp also recovers from problematic
- proxy servers and failed SSL handshakes.</p>
+ and for services hosted in redundant data centers. OkHttp initiates new connections
+ with modern TLS features (SNI, ALPN), and falls back to TLS 1.0 if the handshake
+ fails.</p>
- <p>You can try OkHttp without rewriting your network code. The core module implements
- the familiar <code>java.net.HttpURLConnection</code> API. And the optional
- okhttp-apache module implements the Apache <code>HttpClient</code> API.</p>
+ <p>Using OkHttp is easy. Its 2.0 API is designed with fluent builders and
+ immutability. It supports both synchronous blocking calls and async calls with
+ callbacks.</p>
- <p>OkHttp supports Android 2.2 and above. For Java, the minimum requirement is 1.5.</p>
+ <p>You can try out OkHttp without rewriting your network code. The
+ <code>okhttp-urlconnection</code> module implements the familiar
+ <code>java.net.HttpURLConnection</code> API and the <code>okhttp-apache</code>
+ module implements the Apache <code>HttpClient</code> API.</p>
+
+ <p>OkHttp supports Android 2.3 and above. For Java, the minimum requirement is 1.7.</p>
<h3 id="examples">Examples</h3>
<h4>Get a URL</h4>
<p>This program downloads a URL and print its contents as a string. <a href="https://raw.github.com/square/okhttp/master/samples/guide/src/main/java/com/squareup/okhttp/guide/GetExample.java">Full source</a>.
<pre class="prettyprint">
- OkHttpClient client = new OkHttpClient();
+OkHttpClient client = new OkHttpClient();
- String get(URL url) throws IOException {
- HttpURLConnection connection = client.open(url);
- InputStream in = null;
- try {
- // Read the response.
- in = connection.getInputStream();
- byte[] response = readFully(in);
- return new String(response, "UTF-8");
- } finally {
- if (in != null) in.close();
- }
- }
+String run(String url) throws IOException {
+ Request request = new Request.Builder()
+ .url(url)
+ .build();
+
+ Response response = client.newCall(request).execute();
+ return response.body().string();
+}
</pre>
<h4>Post to a Server</h4>
<p>This program posts data to a service. <a href="https://raw.github.com/square/okhttp/master/samples/guide/src/main/java/com/squareup/okhttp/guide/PostExample.java">Full source</a>.
<pre class="prettyprint">
- OkHttpClient client = new OkHttpClient();
+public static final MediaType JSON
+ = MediaType.parse("application/json; charset=utf-8");
- String post(URL url, byte[] body) throws IOException {
- HttpURLConnection connection = client.open(url);
- OutputStream out = null;
- InputStream in = null;
- try {
- // Write the request.
- connection.setRequestMethod("POST");
- out = connection.getOutputStream();
- out.write(body);
- out.close();
+OkHttpClient client = new OkHttpClient();
- // Read the response.
- if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
- throw new IOException("Unexpected HTTP response: "
- + connection.getResponseCode() + " " + connection.getResponseMessage());
- }
- in = connection.getInputStream();
- return readFirstLine(in);
- } finally {
- // Clean up.
- if (out != null) out.close();
- if (in != null) in.close();
- }
- }
+String post(String url, String json) throws IOException {
+ RequestBody body = RequestBody.create(JSON, json);
+ Request request = new Request.Builder()
+ .url(url)
+ .post(body)
+ .build();
+ Response response = client.newCall(request).execute();
+ return response.body().string();
+}
</pre>
- <!--
- TODO
- Error Handling
- Authentication
- Cookies
- Response Caching
- Captive Gateways
- -->
-
<h3 id="download">Download</h3>
- <p><a href="http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.squareup.okhttp&a=okhttp&v=LATEST" class="dl version-href">↓ <span class="version-tag">Latest</span> JAR</a></p>
- <p>The source code to the OkHttp, its samples, and this website is <a href="http://github.com/square/okhttp">available on GitHub</a>.</p>
+ <p><a href="https://search.maven.org/remote_content?g=com.squareup.okhttp&a=okhttp&v=LATEST" class="dl version-href">↓ <span class="version-tag">Latest</span> JAR</a></p>
+ <p>You'll also need <a href="http://github.com/square/okio">Okio</a>, which OkHttp
+ uses for fast I/O and resizable buffers. Download the
+ <a href="https://search.maven.org/remote_content?g=com.squareup.okio&a=okio&v=LATEST">latest JAR</a>.
+ <p>The source code to OkHttp, its samples, and this website is <a href="http://github.com/square/okhttp">available on GitHub</a>.</p>
<h4>Maven</h4>
<pre class="prettyprint"><dependency>
@@ -143,7 +127,7 @@
<p>Before your code can be accepted into the project you must also sign the <a href="http://squ.re/sign-the-cla">Individual Contributor License Agreement (CLA)</a>.</p>
<h3 id="license">License</h3>
- <pre>Copyright 2013 Square, Inc.
+ <pre>Copyright 2014 Square, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -167,6 +151,7 @@
<li><a href="#license">License</a></li>
</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="http://stackoverflow.com/questions/tagged/okhttp?sort=active">StackOverflow</a></li>
</ul>