aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTobias Thierer <tobiast@google.com>2016-06-24 19:04:17 +0100
committerTobias Thierer <tobiast@google.com>2016-06-29 16:23:09 +0100
commit6c251e20f00c7574b217bd4351ac81666f574380 (patch)
tree2d66a76721f4c8170b990742922675f32ad38122
parent68e16131f12f0174c4c9e5785f6e63297cd02adf (diff)
downloadokhttp-6c251e20f00c7574b217bd4351ac81666f574380.tar.gz
Update OkHttp to 2.7.5 and advance okio by one commit.
This brings OkHttp and okio exactly in line with upstream commits with no local changes. Corresponding upstream commits: okhttp:6e236ce3b80f21369dc544f0e1053ff71be8689b (= parent-2.7.5) okio: 02481cc0cc84bc92e3eab6d5212a226496f56a7e The okio commit differs from the one in the previous pull from Sep 2015 (AOSP commit 71b9f47b26fb57ac3e436a19519c6e3ec70e86eb) only by a single upstream commit, the switch to 8 KiB segments. That commit was previously cherry-picked in AOSP. This CL will temporarily revert the AOSP changes to okio, but those AOSP changes to okio will be reapplied in the subsequent CL. Compilation and tests do not pass after this CL, they will only pass at the end of the chain of 11 CLs going in at the same time. 9 of these 11 CLs are in external/okhttp, the others affect libcore and frameworks/base. Details of behavioural changes introduced by this upgrade are at: https://docs.google.com/document/d/19PF3Exd_q32gAGCiRFWRf0Pq_xrIWs-cRViHkFTxJg8/edit This CL includes files that are not used in Android, such as - top level dot files (.travis.yml etc.) - subdirectories okurl, okhttp-apache, samples, which aren't used - tests in okhttp-hpacktests, okhttp-ws-tests that aren't run or test functionality that we aren't used Test: I've run the following tests *at the end* of the chain of commits, in cts-tradefed: 1.) run cts -p android.core.tests.libcore.package.harmony_java_net 2.) run cts -c libcore.java.net.URLConnectionTest 3.) run cts -p android.core.tests.libcore.package.okhttp 4.) run cts -p android.core.tests.libcore.package.libcore 1.-3.) all passed 4.) had 24 unrelated failures per b/29496407 and b/29744850 Change-Id: Id798d6cf49fa4a7a4ab8ae3b699a38104bf42db3
-rwxr-xr-x.buildscript/deploy_snapshot.sh26
-rw-r--r--.buildscript/settings.xml9
-rw-r--r--.gitignore24
-rw-r--r--.gitmodules3
-rw-r--r--.travis.yml26
-rw-r--r--Android.mk88
-rw-r--r--CHANGELOG.md153
-rw-r--r--README.android22
-rw-r--r--README.md8
-rw-r--r--android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java100
-rw-r--r--android/main/java/com/squareup/okhttp/HttpHandler.java119
-rw-r--r--android/main/java/com/squareup/okhttp/HttpsHandler.java111
-rw-r--r--android/main/java/com/squareup/okhttp/internal/Platform.java135
-rw-r--r--android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java48
-rw-r--r--android/test/java/com/squareup/okhttp/internal/PlatformTest.java207
-rw-r--r--benchmarks/pom.xml2
-rwxr-xr-xdeploy_website.sh23
-rw-r--r--jarjar-rules.txt2
-rw-r--r--mockwebserver/README.md2
-rw-r--r--mockwebserver/pom.xml2
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java143
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java119
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java9
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java16
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java45
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java4
-rw-r--r--mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java5
-rw-r--r--mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java33
-rw-r--r--okcurl/pom.xml2
-rw-r--r--okcurl/src/main/java/com/squareup/okhttp/curl/Main.java2
-rw-r--r--okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java4
-rw-r--r--okhttp-android-support/pom.xml2
-rw-r--r--okhttp-apache/pom.xml2
m---------okhttp-hpacktests/src/test/resources/hpack-test-case0
-rw-r--r--okhttp-logging-interceptor/README.md49
-rw-r--r--okhttp-logging-interceptor/pom.xml40
-rw-r--r--okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java253
-rw-r--r--okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java610
-rw-r--r--okhttp-testing-support/pom.xml2
-rw-r--r--okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java77
-rw-r--r--okhttp-tests/pom.xml2
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java9
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java29
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java438
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java156
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java639
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java250
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java145
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java30
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java65
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java99
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java8
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java17
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java7
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java19
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java76
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java (renamed from okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java)11
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java (renamed from okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java)11
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java6
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java (renamed from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java)35
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java (renamed from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java)2
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java (renamed from okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java)2
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java46
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java58
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java47
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java197
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java255
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java301
-rw-r--r--okhttp-urlconnection/pom.xml2
-rw-r--r--okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java10
-rw-r--r--okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/URLFilter.java32
-rw-r--r--okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java58
-rw-r--r--okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java5
-rw-r--r--okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java49
-rw-r--r--okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java7
-rw-r--r--okhttp-ws-tests/fuzzingserver-config.json153
-rw-r--r--okhttp-ws-tests/fuzzingserver-expected.txt376
-rwxr-xr-xokhttp-ws-tests/fuzzingserver-test.sh28
-rwxr-xr-xokhttp-ws-tests/fuzzingserver-update-expected.sh11
-rw-r--r--okhttp-ws-tests/pom.xml31
-rw-r--r--okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java (renamed from okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java)50
-rw-r--r--okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java342
-rw-r--r--okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java85
-rw-r--r--okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java191
-rw-r--r--okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java56
-rw-r--r--okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java74
-rw-r--r--okhttp-ws/pom.xml2
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java153
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java8
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java58
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java194
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java34
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java61
-rw-r--r--okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java9
-rw-r--r--okhttp/pom.xml7
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Address.java147
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Call.java57
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java6
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Connection.java521
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java364
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java182
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java2
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Dns.java50
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java8
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Headers.java20
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java115
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java103
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Request.java3
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java2
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java45
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/Network.java34
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java171
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/Util.java11
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java110
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java36
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java (renamed from okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java)223
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java (renamed from okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java)212
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java326
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java18
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java (renamed from okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java)16
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java137
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java3
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java10
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java37
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java406
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java407
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java65
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java117
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java58
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java (renamed from android/main/java/com/squareup/okhttp/internal/Version.java)15
-rw-r--r--okio/README.android12
-rw-r--r--okio/okio/src/main/java/okio/DeflaterSink.java5
-rw-r--r--okio/okio/src/main/java/okio/Okio.java32
-rw-r--r--okio/okio/src/test/java/okio/BufferedSinkTest.java10
-rw-r--r--okio/okio/src/test/java/okio/BufferedSourceTest.java10
-rw-r--r--okio/okio/src/test/java/okio/OkioTest.java30
-rw-r--r--okio/okio/src/test/java/okio/ReadUtf8LineTest.java10
-rw-r--r--pom.xml12
-rw-r--r--samples/crawler/pom.xml2
-rw-r--r--samples/guide/pom.xml2
-rw-r--r--samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java2
-rw-r--r--samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java32
-rw-r--r--samples/pom.xml2
-rw-r--r--samples/simple-client/pom.xml2
-rw-r--r--samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java7
-rw-r--r--samples/static-server/pom.xml2
-rw-r--r--website/index.html2
147 files changed, 7299 insertions, 4485 deletions
diff --git a/.buildscript/deploy_snapshot.sh b/.buildscript/deploy_snapshot.sh
new file mode 100755
index 0000000..4e141ca
--- /dev/null
+++ b/.buildscript/deploy_snapshot.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+#
+# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
+#
+# Adapted from https://coderwall.com/p/9b_lfq and
+# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
+
+SLUG="square/okhttp"
+JDK="oraclejdk8"
+BRANCH="master"
+
+set -e
+
+if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then
+ echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'."
+elif [ "$TRAVIS_JDK_VERSION" != "$JDK" ]; then
+ echo "Skipping snapshot deployment: wrong JDK. Expected '$JDK' but was '$TRAVIS_JDK_VERSION'."
+elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+ echo "Skipping snapshot deployment: was pull request."
+elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then
+ echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'."
+else
+ echo "Deploying snapshot..."
+ mvn clean source:jar javadoc:jar deploy --settings=".buildscript/settings.xml" -Dmaven.test.skip=true
+ echo "Snapshot deployed!"
+fi
diff --git a/.buildscript/settings.xml b/.buildscript/settings.xml
new file mode 100644
index 0000000..91f444b
--- /dev/null
+++ b/.buildscript/settings.xml
@@ -0,0 +1,9 @@
+<settings>
+ <servers>
+ <server>
+ <id>sonatype-nexus-snapshots</id>
+ <username>${env.CI_DEPLOY_USERNAME}</username>
+ <password>${env.CI_DEPLOY_PASSWORD}</password>
+ </server>
+ </servers>
+</settings>
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..226a3f3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+.classpath
+.project
+.settings
+eclipsebin
+
+bin
+gen
+build
+out
+lib
+
+target
+pom.xml.*
+release.properties
+
+.idea
+*.iml
+*.ipr
+*.iws
+classes
+
+obj
+
+.DS_Store
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..d29f0b1
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "okhttp-hpacktests/src/test/resources/hpack-test-case"]
+ path = okhttp-hpacktests/src/test/resources/hpack-test-case
+ url = git://github.com/http2jp/hpack-test-case.git
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ed135a7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,26 @@
+language: java
+
+jdk:
+ - oraclejdk7
+ - oraclejdk8
+
+after_success:
+ - .buildscript/deploy_snapshot.sh
+
+env:
+ global:
+ - secure: "S0BTJVrF4fUCwhTdmoQY6LYr5r1wgXZ/p8lc5bIgUUsc1Ckalwt7s/GDwPuLJ4702sI5t56Eye2iEIMUjeFJKqebZRsX1C5oYsYFxGi3BGlepstYpmj0gLXuSWqCLniS9zmHXCxLhLkC6KxPVjhDlbq76XQx0o3K1J8oEIj/PCE="
+ - secure: "awV7yLXURjlPbTOladsNDZk74KYCNXoiZpAP0gQFfK4Sc0fc7+kg8z/yhdWXeTxjsIZ6m0dVDHTqnH8ytnydwXpBam8JdQJ+EAWA6R3Svq1BR1bzl/PcZUoz+Xn8lMXdU3yA1p4qtQlUhMxwsE3MOVe24HSDJPAu4XeWFj1j3qo="
+
+branches:
+ except:
+ - gh-pages
+
+notifications:
+ email: false
+
+sudo: false
+
+cache:
+ directories:
+ - $HOME/.m2
diff --git a/Android.mk b/Android.mk
deleted file mode 100644
index d98f068..0000000
--- a/Android.mk
+++ /dev/null
@@ -1,88 +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.
-#
-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,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,android/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,okhttp-testing-support/src/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-ws/src/main/java)
-okhttp_test_src_files += $(call all-java-files-under,okhttp-ws-tests/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)
-
-# Exclude tests Android currently has problems with:
-# 1) Parameterized (requires JUnit 4.11).
-# 2) New dependencies like gson.
-okhttp_test_src_excludes := \
- okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformUrlTest.java \
- okhttp-tests/src/test/java/com/squareup/okhttp/WebPlatformTestRun.java
-
-okhttp_test_src_files := \
- $(filter-out $(okhttp_test_src_excludes), $(okhttp_test_src_files))
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt
-LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-LOCAL_JAVA_LANGUAGE_VERSION := 1.7
-include $(BUILD_JAVA_LIBRARY)
-
-# non-jarjar'd version of okhttp to compile the tests against
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-nojarjar
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JAVA_LIBRARIES := core-oj core-libart conscrypt
-LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-LOCAL_JAVA_LANGUAGE_VERSION := 1.7
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-tests-nojarjar
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_test_src_files)
-LOCAL_JAVA_LIBRARIES := core-oj core-libart okhttp-nojarjar junit4-target bouncycastle-nojarjar conscrypt
-LOCAL_NO_STANDARD_LIBRARIES := true
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-LOCAL_JAVA_LANGUAGE_VERSION := 1.7
-include $(BUILD_STATIC_JAVA_LIBRARY)
-
-ifeq ($(HOST_OS),linux)
-include $(CLEAR_VARS)
-LOCAL_MODULE := okhttp-hostdex
-LOCAL_MODULE_TAGS := optional
-LOCAL_SRC_FILES := $(okhttp_system_src_files)
-LOCAL_JARJAR_RULES := $(LOCAL_PATH)/jarjar-rules.txt
-LOCAL_JAVA_LIBRARIES := conscrypt-hostdex
-LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk
-LOCAL_JAVA_LANGUAGE_VERSION := 1.7
-include $(BUILD_HOST_DALVIK_JAVA_LIBRARY)
-endif # ($(HOST_OS),linux)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00ad4c3..6ceddcd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,136 @@
Change Log
==========
+## Version 2.7.5
+
+_2016-02-25_
+
+ * Fix: Change the certificate pinner to always build full chains. This
+ prevents a potential crash when using certificate pinning with the Google
+ Play Services security provider.
+
+
+## Version 2.7.4
+
+_2016-02-07_
+
+ * Fix: Don't crash when finding the trust manager if the Play Services (GMS)
+ security provider is installed.
+ * Fix: The previous release introduced a performance regression on Android,
+ caused by looking up CA certificates. This is now fixed.
+
+
+## Version 2.7.3
+
+_2016-02-06_
+
+ * Fix: Permit the trusted CA root to be pinned by `CertificatePinner`.
+
+
+## Version 2.7.2
+
+_2016-01-07_
+
+ * Fix: Don't eagerly release stream allocations on cache hits. We might still
+ need them to handle redirects.
+
+
+## Version 2.7.1
+
+_2016-01-01_
+
+ * Fix: Don't do a health check on newly-created connections. This is
+ unnecessary work that could put the client in an inconsistent state if the
+ health check fails.
+
+
+## Version 2.7.0
+
+_2015-12-12_
+
+ * **Rewritten connection management.** Previously OkHttp's connection pool
+ managed both idle and active connections for HTTP/2, but only idle
+ connections for HTTP/1.x. Wth this update the connection pool manages both
+ idle and active connections for everything. OkHttp now detects and warns on
+ connections that were allocated but never released, and will enforce HTTP/2
+ stream limits. This update also fixes `Call.cancel()` to not do I/O on the
+ calling thread.
+ * Fix: Don't log gzipped data in the logging interceptor.
+ * Fix: Don't resolve DNS addresses when connecting through a SOCKS proxy.
+ * Fix: Drop the synthetic `OkHttp-Selected-Protocol` response header.
+ * Fix: Support 204 and 205 'No Content' replies in the logging interceptor.
+ * New: Add `Call.isExecuted()`.
+
+
+## Version 2.6.0
+
+_2015-11-22_
+
+ * **New Logging Interceptor.** The `logging-interceptor` subproject offers
+ simple request and response logging. It may be configured to log headers and
+ bodies for debugging. It requires this Maven dependency:
+
+ ```xml
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>logging-interceptor</artifactId>
+ <version>2.6.0</version>
+ </dependency>
+ ```
+
+ Configure basic logging like this:
+
+ ```java
+ HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
+ loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);
+ client.networkInterceptors().add(loggingInterceptor);
+ ```
+
+ **Warning:** Avoid `Level.HEADERS` and `Level.BODY` in production because
+ they could leak passwords and other authentication credentials to insecure
+ logs.
+
+ * **WebSocket API now uses `RequestBody` and `ResponseBody` for messages.**
+ This is a backwards-incompatible API change.
+
+ * **The DNS service is now pluggable.** In some situations this may be useful
+ to manually prioritize specific IP addresses.
+
+ * Fix: Don't throw when converting an `HttpUrl` to a `java.net.URI`.
+ Previously URLs with special characters like `|` and `[` would break when
+ subjected to URI’s overly-strict validation.
+ * Fix: Don't re-encode `+` as `%20` in encoded URL query strings. OkHttp
+ prefers `%20` when doing its own encoding, but will retain `+` when that is
+ provided.
+ * Fix: Enforce that callers call `WebSocket.close()` on IO errors. Error
+ handling in WebSockets is significantly improved.
+ * Fix: Don't use SPDY/3 style header concatenation for HTTP/2 request headers.
+ This could have corrupted requests where multiple headers had the same name,
+ as in cookies.
+ * Fix: Reject bad characters in the URL hostname. Previously characters like
+ `\0` would cause a late crash when building the request.
+ * Fix: Allow interceptors to change the request method.
+ * Fix: Don’t use the request's `User-Agent` or `Proxy-Authorization` when
+ connecting to an HTTPS server via an HTTP tunnel. The `Proxy-Authorization`
+ header was being leaked to the origin server.
+ * Fix: Digits may be used in a URL scheme.
+ * Fix: Improve connection timeout recovery.
+ * Fix: Recover from `getsockname` crashes impacting Android releases prior to
+ 4.2.2.
+ * Fix: Drop partial support for HTTP/1.0. Previously OkHttp would send
+ `HTTP/1.0` on connections after seeing a response with `HTTP/1.0`. The fixed
+ behavior is consistent with Firefox and Chrome.
+ * Fix: Allow a body in `OPTIONS` requests.
+ * Fix: Don't percent-encode non-ASCII characters in URL fragments.
+ * Fix: Handle null fragments.
+ * Fix: Don’t crash on interceptors that throw `IOException` before a
+ connection is attempted.
+ * New: Support [WebDAV][webdav] HTTP methods.
+ * New: Buffer WebSocket frames for better performance.
+ * New: Drop support for `TLS_DHE_DSS_WITH_AES_128_CBC_SHA`, our only remaining
+ DSS cipher suite. This is consistent with Firefox and Chrome which have also
+ dropped these cipher suite.
+
## Version 2.5.0
_2015-08-25_
@@ -22,7 +152,7 @@ _2015-08-25_
where changing a URL from `http` to `https` would leave it on port 80.
* **Okio has been updated to 1.6.0.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -81,7 +211,7 @@ _2015-05-16_
Both are permitted-by-spec, but `%20` requires fewer special cases.
* **Okio has been updated to 1.4.0.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -93,7 +223,7 @@ _2015-05-16_
Passing null will now fail for request methods that require a body. Instead
use an empty body such as this one:
- ```
+ ```java
RequestBody.create(null, new byte[0]);
```
@@ -102,7 +232,7 @@ _2015-05-16_
your app. You'll need to pin both the top-level domain and the `*.` domain
for full coverage.
- ```
+ ```java
client.setCertificatePinner(new CertificatePinner.Builder()
.add("publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
.add("*.publicobject.com", "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
@@ -159,7 +289,7 @@ _2015-03-16_
* **Okio updated to 1.3.0.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -264,14 +394,14 @@ _2014-11-04_
To disable TLS fallback:
- ```
+ ```java
client.setConnectionSpecs(Arrays.asList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT));
```
To disable cleartext connections, permitting `https` URLs only:
- ```
+ ```java
client.setConnectionSpecs(Arrays.asList(
ConnectionSpec.MODERN_TLS, ConnectionSpec.COMPATIBLE_TLS));
```
@@ -305,7 +435,7 @@ _2014-11-04_
* **Okio updated to 1.0.1.**
- ```
+ ```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
@@ -385,7 +515,7 @@ advice on upgrading from 1.x to 2.x.
agent.
* New: Guava-like API to create headers:
- ```
+ ```java
Headers headers = Headers.of(name1, value1, name2, value2, ...).
```
@@ -428,7 +558,7 @@ in addition to synchronous blocking calls.
add the `okhttp-urlconnection` module to your project and use the
`OkUrlFactory` to create new instances of `HttpURLConnection`:
- ```
+ ```java
// OkHttp 1.x:
HttpURLConnection connection = client.open(url);
@@ -683,4 +813,5 @@ _2013-05-06_
Initial release.
- [brick]: (https://noncombatant.org/2015/05/01/about-http-public-key-pinning/)
+ [brick]: https://noncombatant.org/2015/05/01/about-http-public-key-pinning/
+ [webdav]: https://tools.ietf.org/html/rfc4918
diff --git a/README.android b/README.android
deleted file mode 100644
index 0a1b91b..0000000
--- a/README.android
+++ /dev/null
@@ -1,22 +0,0 @@
-URL: https://github.com/square/okhttp
-License: Apache 2
-Description: "OkHttp: An HTTP+SPDY client for Android and Java applications."
-
-Local patches
--------------
-
-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.
- - 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 4fde155..f10aaeb 100644
--- a/README.md
+++ b/README.md
@@ -11,12 +11,12 @@ Download [the latest JAR][3] or grab via Maven:
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
- <version>2.5.0</version>
+ <version>2.6.0</version>
</dependency>
```
or Gradle:
```groovy
-compile 'com.squareup.okhttp:okhttp:2.5.0'
+compile 'com.squareup.okhttp:okhttp:2.6.0'
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
@@ -36,13 +36,13 @@ Download [the latest JAR][4] or grab via Maven:
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>mockwebserver</artifactId>
- <version>2.5.0</version>
+ <version>2.6.0</version>
<scope>test</scope>
</dependency>
```
or Gradle:
```groovy
-testCompile 'com.squareup.okhttp:mockwebserver:2.5.0'
+testCompile 'com.squareup.okhttp:mockwebserver:2.6.0'
```
diff --git a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java b/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
deleted file mode 100644
index 36c3101..0000000
--- a/android/main/java/com/squareup/okhttp/ConfigAwareConnectionPool.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import libcore.net.event.NetworkEventDispatcher;
-import libcore.net.event.NetworkEventListener;
-
-/**
- * A provider of the shared Android {@link ConnectionPool}. This class is aware of network
- * configuration change events: When the network configuration changes the pool object is discarded
- * and a later calls to {@link #get()} will return a new pool.
- */
-public class ConfigAwareConnectionPool {
-
- private static final long CONNECTION_POOL_DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min
-
- private static final int CONNECTION_POOL_MAX_IDLE_CONNECTIONS;
- private static final long CONNECTION_POOL_KEEP_ALIVE_DURATION_MS;
- static {
- String keepAliveProperty = System.getProperty("http.keepAlive");
- String keepAliveDurationProperty = System.getProperty("http.keepAliveDuration");
- String maxIdleConnectionsProperty = System.getProperty("http.maxConnections");
- CONNECTION_POOL_KEEP_ALIVE_DURATION_MS = (keepAliveDurationProperty != null
- ? Long.parseLong(keepAliveDurationProperty)
- : CONNECTION_POOL_DEFAULT_KEEP_ALIVE_DURATION_MS);
- if (keepAliveProperty != null && !Boolean.parseBoolean(keepAliveProperty)) {
- CONNECTION_POOL_MAX_IDLE_CONNECTIONS = 0;
- } else if (maxIdleConnectionsProperty != null) {
- CONNECTION_POOL_MAX_IDLE_CONNECTIONS = Integer.parseInt(maxIdleConnectionsProperty);
- } else {
- CONNECTION_POOL_MAX_IDLE_CONNECTIONS = 5;
- }
- }
-
- private static final ConfigAwareConnectionPool instance = new ConfigAwareConnectionPool();
-
- private final NetworkEventDispatcher networkEventDispatcher;
-
- /**
- * {@code true} if the ConnectionPool reset has been registered with the
- * {@link NetworkEventDispatcher}.
- */
- private boolean networkEventListenerRegistered;
-
- private ConnectionPool connectionPool;
-
- /** Visible for testing. Use {@link #getInstance()} */
- protected ConfigAwareConnectionPool(NetworkEventDispatcher networkEventDispatcher) {
- this.networkEventDispatcher = networkEventDispatcher;
- }
-
- private ConfigAwareConnectionPool() {
- networkEventDispatcher = NetworkEventDispatcher.getInstance();
- }
-
- public static ConfigAwareConnectionPool getInstance() {
- return instance;
- }
-
- /**
- * Returns the current {@link ConnectionPool} to use.
- */
- public synchronized ConnectionPool get() {
- if (connectionPool == null) {
- // Only register the listener once the first time a ConnectionPool is created.
- if (!networkEventListenerRegistered) {
- networkEventDispatcher.addListener(new NetworkEventListener() {
- @Override
- public void onNetworkConfigurationChanged() {
- synchronized (ConfigAwareConnectionPool.this) {
- // 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 = null;
- }
- }
- });
- networkEventListenerRegistered = true;
- }
- connectionPool = new ConnectionPool(
- CONNECTION_POOL_MAX_IDLE_CONNECTIONS, CONNECTION_POOL_KEEP_ALIVE_DURATION_MS);
- }
- return connectionPool;
- }
-}
diff --git a/android/main/java/com/squareup/okhttp/HttpHandler.java b/android/main/java/com/squareup/okhttp/HttpHandler.java
deleted file mode 100644
index 38eecb4..0000000
--- a/android/main/java/com/squareup/okhttp/HttpHandler.java
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import com.squareup.okhttp.internal.URLFilter;
-import libcore.net.NetworkSecurityPolicy;
-import java.io.IOException;
-import java.net.HttpURLConnection;
-import java.net.Proxy;
-import java.net.ResponseCache;
-import java.net.URL;
-import java.net.URLConnection;
-import java.net.URLStreamHandler;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-public class HttpHandler extends URLStreamHandler {
-
- private final static List<ConnectionSpec> CLEARTEXT_ONLY =
- Collections.singletonList(ConnectionSpec.CLEARTEXT);
-
- private static final CleartextURLFilter CLEARTEXT_FILTER = new CleartextURLFilter();
-
- private final ConfigAwareConnectionPool configAwareConnectionPool =
- ConfigAwareConnectionPool.getInstance();
-
- @Override protected URLConnection openConnection(URL url) throws IOException {
- 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 newOkUrlFactory(proxy).open(url);
- }
-
- @Override protected int getDefaultPort() {
- return 80;
- }
-
- protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
- OkUrlFactory okUrlFactory = createHttpOkUrlFactory(proxy);
- // For HttpURLConnections created through java.net.URL Android uses a connection pool that
- // is aware when the default network changes so that pooled connections are not re-used when
- // the default network changes.
- okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
- return okUrlFactory;
- }
-
- /**
- * Creates an OkHttpClient suitable for creating {@link java.net.HttpURLConnection} instances on
- * Android.
- */
- // Visible for android.net.Network.
- public static OkUrlFactory createHttpOkUrlFactory(Proxy proxy) {
- OkHttpClient client = new OkHttpClient();
-
- // Explicitly set the timeouts to infinity.
- client.setConnectTimeout(0, TimeUnit.MILLISECONDS);
- client.setReadTimeout(0, TimeUnit.MILLISECONDS);
- client.setWriteTimeout(0, TimeUnit.MILLISECONDS);
-
- // Set the default (same protocol) redirect behavior. The default can be overridden for
- // each instance using HttpURLConnection.setInstanceFollowRedirects().
- client.setFollowRedirects(HttpURLConnection.getFollowRedirects());
-
- // Do not permit http -> https and https -> http redirects.
- client.setFollowSslRedirects(false);
-
- // Permit cleartext traffic only (this is a handler for HTTP, not for HTTPS).
- client.setConnectionSpecs(CLEARTEXT_ONLY);
-
- // When we do not set the Proxy explicitly OkHttp picks up a ProxySelector using
- // ProxySelector.getDefault().
- if (proxy != null) {
- client.setProxy(proxy);
- }
-
- // OkHttp requires that we explicitly set the response cache.
- OkUrlFactory okUrlFactory = new OkUrlFactory(client);
-
- // Use the installed NetworkSecurityPolicy to determine which requests are permitted over
- // http.
- okUrlFactory.setUrlFilter(CLEARTEXT_FILTER);
-
- ResponseCache responseCache = ResponseCache.getDefault();
- if (responseCache != null) {
- AndroidInternal.setResponseCache(okUrlFactory, responseCache);
- }
- return okUrlFactory;
- }
-
- private static final class CleartextURLFilter implements URLFilter {
- @Override
- public void checkURLPermitted(URL url) throws IOException {
- String host = url.getHost();
- if (!NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted(host)) {
- throw new IOException("Cleartext HTTP traffic to " + host + " not permitted");
- }
- }
- }
-}
diff --git a/android/main/java/com/squareup/okhttp/HttpsHandler.java b/android/main/java/com/squareup/okhttp/HttpsHandler.java
deleted file mode 100644
index 6b127b2..0000000
--- a/android/main/java/com/squareup/okhttp/HttpsHandler.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import java.net.Proxy;
-import java.util.Arrays;
-import java.util.List;
-
-import javax.net.ssl.HttpsURLConnection;
-
-public final class HttpsHandler extends HttpHandler {
-
- /**
- * The initial connection spec to use when connecting to an https:// server, and the prototype
- * for the others below. Note that Android does not set the cipher suites to use so the socket's
- * defaults enabled cipher suites will be used instead. When the SSLSocketFactory is provided by
- * the app or GMS core we will not override the enabled ciphers set on the sockets it produces
- * with a list hardcoded at release time. This is deliberate.
- * For the TLS versions we <em>will</em> select a known subset from the set of enabled TLS
- * versions on the socket.
- */
- private static final ConnectionSpec TLS_1_2_AND_BELOW = new ConnectionSpec.Builder(true)
- .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
- .supportsTlsExtensions(true)
- .build();
-
- private static final ConnectionSpec TLS_1_1_AND_BELOW =
- new ConnectionSpec.Builder(TLS_1_2_AND_BELOW)
- .tlsVersions(TlsVersion.TLS_1_1, TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
- .supportsTlsExtensions(true)
- .build();
-
- private static final ConnectionSpec TLS_1_0_AND_BELOW =
- new ConnectionSpec.Builder(TLS_1_2_AND_BELOW)
- .tlsVersions(TlsVersion.TLS_1_0, TlsVersion.SSL_3_0)
- .build();
-
- private static final ConnectionSpec SSL_3_0 =
- new ConnectionSpec.Builder(TLS_1_2_AND_BELOW)
- .tlsVersions(TlsVersion.SSL_3_0)
- .build();
-
- /** Try up to 4 times to negotiate a connection with each server. */
- private static final List<ConnectionSpec> SECURE_CONNECTION_SPECS =
- Arrays.asList(TLS_1_2_AND_BELOW, TLS_1_1_AND_BELOW, TLS_1_0_AND_BELOW, SSL_3_0);
-
- private static final List<Protocol> HTTP_1_1_ONLY = Arrays.asList(Protocol.HTTP_1_1);
-
- private final ConfigAwareConnectionPool configAwareConnectionPool =
- ConfigAwareConnectionPool.getInstance();
-
- @Override protected int getDefaultPort() {
- return 443;
- }
-
- @Override
- protected OkUrlFactory newOkUrlFactory(Proxy proxy) {
- OkUrlFactory okUrlFactory = createHttpsOkUrlFactory(proxy);
- // For HttpsURLConnections created through java.net.URL Android uses a connection pool that
- // is aware when the default network changes so that pooled connections are not re-used when
- // the default network changes.
- okUrlFactory.client().setConnectionPool(configAwareConnectionPool.get());
- return okUrlFactory;
- }
-
- /**
- * Creates an OkHttpClient suitable for creating {@link HttpsURLConnection} instances on
- * Android.
- */
- // Visible for android.net.Network.
- public static OkUrlFactory createHttpsOkUrlFactory(Proxy proxy) {
- // The HTTPS OkHttpClient is an HTTP OkHttpClient with extra configuration.
- OkUrlFactory okUrlFactory = HttpHandler.createHttpOkUrlFactory(proxy);
-
- // All HTTPS requests are allowed.
- okUrlFactory.setUrlFilter(null);
-
- OkHttpClient okHttpClient = okUrlFactory.client();
-
- // Only enable HTTP/1.1 (implies HTTP/1.0). Disable SPDY / HTTP/2.0.
- okHttpClient.setProtocols(HTTP_1_1_ONLY);
-
- // Use Android's preferred fallback approach and cipher suite selection.
- okHttpClient.setConnectionSpecs(SECURE_CONNECTION_SPECS);
-
- // OkHttp does not automatically honor the system-wide HostnameVerifier set with
- // HttpsURLConnection.setDefaultHostnameVerifier().
- 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.
- okHttpClient.setSslSocketFactory(HttpsURLConnection.getDefaultSSLSocketFactory());
-
- return okUrlFactory;
- }
-}
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
deleted file mode 100644
index 322c875..0000000
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Copyright (C) 2012 Square, Inc.
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-
-import dalvik.system.SocketTagger;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.net.InetSocketAddress;
-import java.net.Socket;
-import java.net.SocketException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.util.List;
-import javax.net.ssl.SSLSocket;
-
-import com.squareup.okhttp.Protocol;
-
-import okio.Buffer;
-
-/**
- * Access to proprietary Android APIs. Doesn't use reflection.
- */
-public final class Platform {
- private static final Platform PLATFORM = new Platform();
-
- public static Platform get() {
- return PLATFORM;
- }
-
- /** 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 );
-
- public void logW(String warning) {
- System.logW(warning);
- }
-
- public void tagSocket(Socket socket) throws SocketException {
- SocketTagger.get().tag(socket);
- }
-
- public void untagSocket(Socket socket) throws SocketException {
- SocketTagger.get().untag(socket);
- }
-
- 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);
- }
-
- // Enable ALPN.
- boolean alpnSupported = SET_ALPN_PROTOCOLS.isSupported(sslSocket);
- if (!alpnSupported) {
- return;
- }
-
- Object[] parameters = { concatLengthPrefixed(protocols) };
- if (alpnSupported) {
- SET_ALPN_PROTOCOLS.invokeWithoutCheckedException(sslSocket, parameters);
- }
- }
-
- /**
- * Called after the TLS handshake to release resources allocated by {@link
- * #configureTlsExtensions}.
- */
- 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);
- if (alpnResult != null) {
- return new String(alpnResult, Util.UTF_8);
- }
- return null;
- }
-
- public void connectSocket(Socket socket, InetSocketAddress address,
- int connectTimeout) throws IOException {
- socket.connect(address, connectTimeout);
- }
-
- /** Prefix used on custom headers. */
- public String getPrefix() {
- return "X-Android";
- }
-
- /**
- * 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) {
- 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());
- }
- return result.readByteArray();
- }
-}
diff --git a/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java b/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
deleted file mode 100644
index 825f980..0000000
--- a/android/test/java/com/squareup/okhttp/ConfigAwareConnectionPoolTest.java
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp;
-
-import org.junit.Test;
-
-import libcore.net.event.NetworkEventDispatcher;
-
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
-
-/**
- * Tests for {@link ConfigAwareConnectionPool}.
- */
-public class ConfigAwareConnectionPoolTest {
-
- @Test
- public void getInstance() {
- assertSame(ConfigAwareConnectionPool.getInstance(), ConfigAwareConnectionPool.getInstance());
- }
-
- @Test
- public void get() throws Exception {
- NetworkEventDispatcher networkEventDispatcher = new NetworkEventDispatcher() {};
- ConfigAwareConnectionPool instance = new ConfigAwareConnectionPool(networkEventDispatcher) {};
- assertSame(instance.get(), instance.get());
-
- ConnectionPool beforeEventInstance = instance.get();
- networkEventDispatcher.onNetworkConfigurationChanged();
-
- assertNotSame(beforeEventInstance, instance.get());
- }
-}
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
deleted file mode 100644
index 2e5dcdd..0000000
--- a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
+++ /dev/null
@@ -1,207 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp.internal;
-
-import com.android.org.conscrypt.OpenSSLSocketImpl;
-import com.squareup.okhttp.Protocol;
-
-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.assertNull;
-import static org.junit.Assert.assertTrue;
-
-/**
- * Tests for {@link Platform}.
- */
-public class PlatformTest {
-
- @Test
- public void enableTlsExtensionOptionalMethods() throws Exception {
- Platform platform = new Platform();
-
- // Expect no error
- TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
- 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.configureTlsExtensions(openSslSocket, "host", protocols);
- assertTrue(openSslSocket.useSessionTickets);
- assertEquals("host", openSslSocket.hostname);
- assertArrayEquals(Platform.concatLengthPrefixed(protocols), openSslSocket.alpnProtocols);
- }
-
- @Test
- public void getSelectedProtocol() throws Exception {
- Platform platform = new Platform();
- String selectedProtocol = "alpn";
-
- TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
- assertNull(platform.getSelectedProtocol(arbitrarySocketImpl));
-
- NpnOnlySSLSocketImpl npnOnlySSLSocketImpl = new NpnOnlySSLSocketImpl();
- assertNull(platform.getSelectedProtocol(npnOnlySSLSocketImpl));
-
- FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
- openSslSocket.alpnProtocols = selectedProtocol.getBytes(StandardCharsets.UTF_8);
- assertEquals(selectedProtocol, platform.getSelectedProtocol(openSslSocket));
- }
-
- private static class FullOpenSSLSocketImpl extends OpenSSLSocketImpl {
- private boolean useSessionTickets;
- private String hostname;
- private byte[] alpnProtocols;
-
- public FullOpenSSLSocketImpl() throws IOException {
- super(null);
- }
-
- @Override
- public void setUseSessionTickets(boolean useSessionTickets) {
- this.useSessionTickets = useSessionTickets;
- }
-
- @Override
- public void setHostname(String hostname) {
- this.hostname = hostname;
- }
-
- @Override
- public void setAlpnProtocols(byte[] alpnProtocols) {
- this.alpnProtocols = alpnProtocols;
- }
-
- @Override
- public byte[] getAlpnSelectedProtocol() {
- return alpnProtocols;
- }
- }
-
- // 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
- public String[] getSupportedCipherSuites() {
- return new String[0];
- }
-
- @Override
- public String[] getEnabledCipherSuites() {
- return new String[0];
- }
-
- @Override
- public void setEnabledCipherSuites(String[] suites) {
- }
-
- @Override
- public String[] getSupportedProtocols() {
- return new String[0];
- }
-
- @Override
- public String[] getEnabledProtocols() {
- return new String[0];
- }
-
- @Override
- public void setEnabledProtocols(String[] protocols) {
- }
-
- @Override
- public SSLSession getSession() {
- return null;
- }
-
- @Override
- public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
- }
-
- @Override
- public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
- }
-
- @Override
- public void startHandshake() throws IOException {
- }
-
- @Override
- public void setUseClientMode(boolean mode) {
- }
-
- @Override
- public boolean getUseClientMode() {
- return false;
- }
-
- @Override
- public void setNeedClientAuth(boolean need) {
- }
-
- @Override
- public void setWantClientAuth(boolean want) {
- }
-
- @Override
- public boolean getNeedClientAuth() {
- return false;
- }
-
- @Override
- public boolean getWantClientAuth() {
- return false;
- }
-
- @Override
- public void setEnableSessionCreation(boolean flag) {
- }
-
- @Override
- public boolean getEnableSessionCreation() {
- return false;
- }
- }
-}
diff --git a/benchmarks/pom.xml b/benchmarks/pom.xml
index d0d2566..b938848 100644
--- a/benchmarks/pom.xml
+++ b/benchmarks/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>benchmarks</artifactId>
diff --git a/deploy_website.sh b/deploy_website.sh
index bbeedc2..c9b7f15 100755
--- a/deploy_website.sh
+++ b/deploy_website.sh
@@ -3,9 +3,6 @@
set -ex
REPO="git@github.com:square/okhttp.git"
-GROUP_ID="com.squareup.okhttp"
-ARTIFACT_ID="okhttp"
-
DIR=temp-clone
# Delete any existing temporary website clone
@@ -20,28 +17,12 @@ cd $DIR
# Checkout and track the gh-pages branch
git checkout -t origin/gh-pages
-# Delete everything
-rm -rf *
+# Delete everything that isn't versioned (1.x, 2.x)
+ls | grep -E -v '^\d+\.x$' | xargs rm -rf
# Copy website files from real repo
cp -R ../website/* .
-# Download the latest javadoc to directories like 'javadoc' or 'javadoc-urlconnection'.
-for DOCUMENTED_ARTIFACT in okhttp okhttp-urlconnection okhttp-apache
-do
- curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$DOCUMENTED_ARTIFACT&v=LATEST&c=javadoc" > javadoc.zip
- JAVADOC_DIR="javadoc${DOCUMENTED_ARTIFACT//okhttp/}"
- mkdir $JAVADOC_DIR
- unzip javadoc.zip -d $JAVADOC_DIR
- rm javadoc.zip
-done
-
-# Download the 1.6.0 javadoc to '1.x/javadoc'.
-curl -L "https://search.maven.org/remote_content?g=$GROUP_ID&a=$ARTIFACT_ID&v=1.6.0&c=javadoc" > javadoc.zip
-mkdir -p 1.x/javadoc
-unzip javadoc.zip -d 1.x/javadoc
-rm javadoc.zip
-
# Stage all files in git and create a commit
git add .
git add -u
diff --git a/jarjar-rules.txt b/jarjar-rules.txt
deleted file mode 100644
index c84813d..0000000
--- a/jarjar-rules.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-rule com.squareup.** com.android.@1
-rule okio.** com.android.okhttp.okio.@1
diff --git a/mockwebserver/README.md b/mockwebserver/README.md
index 3fd6ca6..ba40bf5 100644
--- a/mockwebserver/README.md
+++ b/mockwebserver/README.md
@@ -42,7 +42,7 @@ public void test() throws Exception {
server.start();
// Ask the server for its URL. You'll need this to make HTTP requests.
- URL baseUrl = server.url("/v1/chat/");
+ HttpUrl baseUrl = server.url("/v1/chat/");
// Exercise your application code, which should make those HTTP requests.
// Responses are returned in the same order that they are enqueued.
diff --git a/mockwebserver/pom.xml b/mockwebserver/pom.xml
index 9ef5211..7681ad1 100644
--- a/mockwebserver/pom.xml
+++ b/mockwebserver/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>mockwebserver</artifactId>
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java
new file mode 100644
index 0000000..2fff99c
--- /dev/null
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/HeldCertificate.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import java.math.BigInteger;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
+import java.security.Security;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.UUID;
+import javax.security.auth.x500.X500Principal;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.X509Extensions;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.bouncycastle.x509.X509V3CertificateGenerator;
+
+/**
+ * A certificate and its private key. This can be used on the server side by HTTPS servers, or on
+ * the client side to verify those HTTPS servers. A held certificate can also be used to sign other
+ * held certificates, as done in practice by certificate authorities.
+ */
+public final class HeldCertificate {
+ public final X509Certificate certificate;
+ public final KeyPair keyPair;
+
+ public HeldCertificate(X509Certificate certificate, KeyPair keyPair) {
+ this.certificate = certificate;
+ this.keyPair = keyPair;
+ }
+
+ public static final class Builder {
+ static {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ private final long duration = 1000L * 60 * 60 * 24; // One day.
+ private String hostname;
+ private String serialNumber = "1";
+ private KeyPair keyPair;
+ private HeldCertificate issuedBy;
+ private int maxIntermediateCas;
+
+ public Builder serialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ return this;
+ }
+
+ /**
+ * Set this certificate's name. Typically this is the URL hostname for TLS certificates. This is
+ * the CN (common name) in the certificate. Will be a random string if no value is provided.
+ */
+ public Builder commonName(String hostname) {
+ this.hostname = hostname;
+ return this;
+ }
+
+ public Builder keyPair(KeyPair keyPair) {
+ this.keyPair = keyPair;
+ return this;
+ }
+
+ /**
+ * Set the certificate that signs this certificate. If unset, a self-signed certificate will be
+ * generated.
+ */
+ public Builder issuedBy(HeldCertificate signedBy) {
+ this.issuedBy = signedBy;
+ return this;
+ }
+
+ /**
+ * Set this certificate to be a certificate authority, with up to {@code maxIntermediateCas}
+ * intermediate certificate authorities beneath it.
+ */
+ public Builder ca(int maxIntermediateCas) {
+ this.maxIntermediateCas = maxIntermediateCas;
+ return this;
+ }
+
+ public HeldCertificate build() throws GeneralSecurityException {
+ // Subject, public & private keys for this certificate.
+ KeyPair heldKeyPair = keyPair != null
+ ? keyPair
+ : generateKeyPair();
+ X500Principal subject = hostname != null
+ ? new X500Principal("CN=" + hostname)
+ : new X500Principal("CN=" + UUID.randomUUID());
+
+ // Subject, public & private keys for this certificate's signer. It may be self signed!
+ KeyPair signedByKeyPair;
+ X500Principal signedByPrincipal;
+ if (issuedBy != null) {
+ signedByKeyPair = issuedBy.keyPair;
+ signedByPrincipal = issuedBy.certificate.getSubjectX500Principal();
+ } else {
+ signedByKeyPair = heldKeyPair;
+ signedByPrincipal = subject;
+ }
+
+ // Generate & sign the certificate.
+ long now = System.currentTimeMillis();
+ X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
+ generator.setSerialNumber(new BigInteger(serialNumber));
+ generator.setIssuerDN(signedByPrincipal);
+ generator.setNotBefore(new Date(now));
+ generator.setNotAfter(new Date(now + duration));
+ generator.setSubjectDN(subject);
+ generator.setPublicKey(heldKeyPair.getPublic());
+ generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
+
+ if (maxIntermediateCas > 0) {
+ generator.addExtension(X509Extensions.BasicConstraints, true,
+ new BasicConstraints(maxIntermediateCas));
+ }
+
+ X509Certificate certificate = generator.generateX509Certificate(
+ signedByKeyPair.getPrivate(), "BC");
+ return new HeldCertificate(certificate, heldKeyPair);
+ }
+
+ public KeyPair generateKeyPair() throws GeneralSecurityException {
+ KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
+ keyPairGenerator.initialize(1024, new SecureRandom());
+ return keyPairGenerator.generateKeyPair();
+ }
+ }
+}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
index 546d660..fd1d020 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/SslContextBuilder.java
@@ -18,24 +18,18 @@ package com.squareup.okhttp.internal;
import java.io.IOException;
import java.io.InputStream;
-import java.math.BigInteger;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.SecureRandom;
-import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
-import java.util.Date;
+import java.util.ArrayList;
+import java.util.List;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
-import javax.security.auth.x500.X500Principal;
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-import org.bouncycastle.x509.X509V3CertificateGenerator;
/**
* Constructs an SSL context for testing. This uses Bouncy Castle to generate a
@@ -45,51 +39,70 @@ import org.bouncycastle.x509.X509V3CertificateGenerator;
* reuse SSL context instances where possible.
*/
public final class SslContextBuilder {
- static {
- Security.addProvider(new BouncyCastleProvider());
- }
-
- private static final long ONE_DAY_MILLIS = 1000L * 60 * 60 * 24;
private static SSLContext localhost; // Lazily initialized.
- private final String hostName;
- private long notBefore = System.currentTimeMillis();
- private long notAfter = System.currentTimeMillis() + ONE_DAY_MILLIS;
+ /** Returns a new SSL context for this host's current localhost address. */
+ public static synchronized SSLContext localhost() {
+ if (localhost != null) return localhost;
+
+ try {
+ // Generate a self-signed cert for the server to serve and the client to trust.
+ HeldCertificate heldCertificate = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .commonName(InetAddress.getByName("localhost").getHostName())
+ .build();
+
+ localhost = new SslContextBuilder()
+ .certificateChain(heldCertificate)
+ .addTrustedCertificate(heldCertificate.certificate)
+ .build();
+
+ return localhost;
+ } catch (GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ } catch (UnknownHostException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private HeldCertificate[] chain;
+ private List<X509Certificate> trustedCertificates = new ArrayList<>();
/**
- * @param hostName the subject of the host. For TLS this should be the
- * domain name that the client uses to identify the server.
+ * Configure the certificate chain to use when serving HTTPS responses. The first certificate
+ * in this chain is the server's certificate, further certificates are included in the handshake
+ * so the client can build a trusted path to a CA certificate.
*/
- public SslContextBuilder(String hostName) {
- this.hostName = hostName;
+ public SslContextBuilder certificateChain(HeldCertificate... chain) {
+ this.chain = chain;
+ return this;
}
- /** Returns a new SSL context for this host's current localhost address. */
- public static synchronized SSLContext localhost() {
- if (localhost == null) {
- try {
- localhost = new SslContextBuilder(InetAddress.getByName("localhost").getHostName()).build();
- } catch (GeneralSecurityException e) {
- throw new RuntimeException(e);
- } catch (UnknownHostException e) {
- throw new RuntimeException(e);
- }
- }
- return localhost;
+ /**
+ * Add a certificate authority that this client trusts. Servers that provide certificate chains
+ * signed by these roots (or their intermediates) will be accepted.
+ */
+ public SslContextBuilder addTrustedCertificate(X509Certificate certificate) {
+ trustedCertificates.add(certificate);
+ return this;
}
public SSLContext build() throws GeneralSecurityException {
+ // Put the certificate in a key store.
char[] password = "password".toCharArray();
+ KeyStore keyStore = newEmptyKeyStore(password);
- // Generate public and private keys and use them to make a self-signed certificate.
- KeyPair keyPair = generateKeyPair();
- X509Certificate certificate = selfSignedCertificate(keyPair, "1");
+ if (chain != null) {
+ Certificate[] certificates = new Certificate[chain.length];
+ for (int i = 0; i < chain.length; i++) {
+ certificates[i] = chain[i].certificate;
+ }
+ keyStore.setKeyEntry("private", chain[0].keyPair.getPrivate(), password, certificates);
+ }
- // Put 'em in a key store.
- KeyStore keyStore = newEmptyKeyStore(password);
- Certificate[] certificateChain = { certificate };
- keyStore.setKeyEntry("private", keyPair.getPrivate(), password, certificateChain);
- keyStore.setCertificateEntry("cert", certificate);
+ for (int i = 0; i < trustedCertificates.size(); i++) {
+ keyStore.setCertificateEntry("cert_" + i, trustedCertificates.get(i));
+ }
// Wrap it up in an SSL context.
KeyManagerFactory keyManagerFactory =
@@ -104,32 +117,6 @@ public final class SslContextBuilder {
return sslContext;
}
- public KeyPair generateKeyPair() throws GeneralSecurityException {
- KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC");
- keyPairGenerator.initialize(1024, new SecureRandom());
- return keyPairGenerator.generateKeyPair();
- }
-
- /**
- * Generates a certificate for {@code hostName} containing {@code keyPair}'s
- * public key, signed by {@code keyPair}'s private key.
- */
- @SuppressWarnings("deprecation") // use the old Bouncy Castle APIs to reduce dependencies.
- public X509Certificate selfSignedCertificate(KeyPair keyPair, String serialNumber)
- throws GeneralSecurityException {
- X509V3CertificateGenerator generator = new X509V3CertificateGenerator();
- X500Principal issuer = new X500Principal("CN=" + hostName);
- X500Principal subject = new X500Principal("CN=" + hostName);
- generator.setSerialNumber(new BigInteger(serialNumber));
- generator.setIssuerDN(issuer);
- generator.setNotBefore(new Date(notBefore));
- generator.setNotAfter(new Date(notAfter));
- generator.setSubjectDN(subject);
- generator.setPublicKey(keyPair.getPublic());
- generator.setSignatureAlgorithm("SHA256WithRSAEncryption");
- return generator.generateX509Certificate(keyPair.getPrivate(), "BC");
- }
-
private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
index b95b64d..1574806 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/internal/framed/FramedServer.java
@@ -36,7 +36,7 @@ import okio.Okio;
import okio.Source;
/** A basic SPDY/HTTP_2 server that serves the contents of a local directory. */
-public final class FramedServer implements IncomingStreamHandler {
+public final class FramedServer extends FramedConnection.Listener {
static final Logger logger = Logger.getLogger(FramedServer.class.getName());
private final List<Protocol> framedProtocols =
@@ -65,9 +65,10 @@ public final class FramedServer implements IncomingStreamHandler {
if (protocol == null || !framedProtocols.contains(protocol)) {
throw new ProtocolException("Protocol " + protocol + " unsupported");
}
- FramedConnection framedConnection = new FramedConnection.Builder(false, sslSocket)
+ FramedConnection framedConnection = new FramedConnection.Builder(false)
+ .socket(sslSocket)
.protocol(protocol)
- .handler(this)
+ .listener(this)
.build();
framedConnection.sendConnectionPreface();
} catch (IOException e) {
@@ -89,7 +90,7 @@ public final class FramedServer implements IncomingStreamHandler {
return sslSocket;
}
- @Override public void receive(final FramedStream stream) throws IOException {
+ @Override public void onStream(final FramedStream stream) throws IOException {
try {
List<Header> requestHeaders = stream.getRequestHeaders();
String path = null;
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
index bc43bd4..db68595 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockResponse.java
@@ -17,6 +17,7 @@ package com.squareup.okhttp.mockwebserver;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.framed.Settings;
import com.squareup.okhttp.ws.WebSocketListener;
import java.util.ArrayList;
import java.util.List;
@@ -42,6 +43,7 @@ public final class MockResponse implements Cloneable {
private TimeUnit bodyDelayUnit = TimeUnit.MILLISECONDS;
private List<PushPromise> promises = new ArrayList<>();
+ private Settings settings;
private WebSocketListener webSocketListener;
/** Creates a new mock response with an empty body. */
@@ -242,6 +244,20 @@ public final class MockResponse implements Cloneable {
}
/**
+ * When {@linkplain MockWebServer#setProtocols(java.util.List) protocols}
+ * include {@linkplain com.squareup.okhttp.Protocol#HTTP_2 HTTP/2}, this
+ * pushes {@code settings} before writing the response.
+ */
+ public MockResponse withSettings(Settings settings) {
+ this.settings = settings;
+ return this;
+ }
+
+ public Settings getSettings() {
+ return settings;
+ }
+
+ /**
* Attempts to perform a web socket upgrade on the connection. This will overwrite any previously
* set status or body.
*/
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
index 0e746d3..2c76398 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/MockWebServer.java
@@ -29,7 +29,7 @@ import com.squareup.okhttp.internal.framed.ErrorCode;
import com.squareup.okhttp.internal.framed.FramedConnection;
import com.squareup.okhttp.internal.framed.FramedStream;
import com.squareup.okhttp.internal.framed.Header;
-import com.squareup.okhttp.internal.framed.IncomingStreamHandler;
+import com.squareup.okhttp.internal.framed.Settings;
import com.squareup.okhttp.internal.http.HttpMethod;
import com.squareup.okhttp.internal.ws.RealWebSocket;
import com.squareup.okhttp.internal.ws.WebSocketProtocol;
@@ -82,6 +82,7 @@ import org.junit.runner.Description;
import org.junit.runners.model.Statement;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_AT_START;
+import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_REQUEST_BODY;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY;
import static com.squareup.okhttp.mockwebserver.SocketPolicy.FAIL_HANDSHAKE;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -174,8 +175,10 @@ public final class MockWebServer implements TestRule {
}
public void setServerSocketFactory(ServerSocketFactory serverSocketFactory) {
- if (executor != null) throw new IllegalStateException(
- "setServerSocketFactory() must be called before start()");
+ if (executor != null) {
+ throw new IllegalStateException(
+ "setServerSocketFactory() must be called before start()");
+ }
this.serverSocketFactory = serverSocketFactory;
}
@@ -463,11 +466,12 @@ public final class MockWebServer implements TestRule {
}
if (protocol != Protocol.HTTP_1_1) {
- FramedSocketHandler framedSocketHandler = new FramedSocketHandler(socket, protocol);
- FramedConnection framedConnection =
- new FramedConnection.Builder(false, socket).protocol(protocol)
- .handler(framedSocketHandler)
- .build();
+ FramedSocketHandler framedSocketListener = new FramedSocketHandler(socket, protocol);
+ FramedConnection framedConnection = new FramedConnection.Builder(false)
+ .socket(socket)
+ .protocol(protocol)
+ .listener(framedSocketListener)
+ .build();
openFramedConnections.add(framedConnection);
openClientSockets.remove(socket);
return;
@@ -672,7 +676,7 @@ public final class MockWebServer implements TestRule {
final RealWebSocket webSocket =
new RealWebSocket(false /* is server */, source, sink, new SecureRandom(), replyExecutor,
listener, request.getPath()) {
- @Override protected void closeConnection() throws IOException {
+ @Override protected void close() throws IOException {
connectionClose.countDown();
}
};
@@ -704,6 +708,7 @@ public final class MockWebServer implements TestRule {
throw new RuntimeException(e);
}
+ replyExecutor.shutdown();
Util.closeQuietly(sink);
Util.closeQuietly(source);
}
@@ -754,8 +759,9 @@ public final class MockWebServer implements TestRule {
long periodDelayMs = policy.getThrottlePeriod(TimeUnit.MILLISECONDS);
long halfByteCount = byteCount / 2;
- boolean disconnectHalfway =
- !isRequest && policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
+ boolean disconnectHalfway = isRequest
+ ? policy.getSocketPolicy() == DISCONNECT_DURING_REQUEST_BODY
+ : policy.getSocketPolicy() == DISCONNECT_DURING_RESPONSE_BODY;
while (!socket.isClosed()) {
for (int b = 0; b < bytesPerPeriod; ) {
@@ -847,7 +853,7 @@ public final class MockWebServer implements TestRule {
}
/** Processes HTTP requests layered over framed protocols. */
- private class FramedSocketHandler implements IncomingStreamHandler {
+ private class FramedSocketHandler extends FramedConnection.Listener {
private final Socket socket;
private final Protocol protocol;
private final AtomicInteger sequenceNumber = new AtomicInteger();
@@ -857,7 +863,7 @@ public final class MockWebServer implements TestRule {
this.protocol = protocol;
}
- @Override public void receive(FramedStream stream) throws IOException {
+ @Override public void onStream(FramedStream stream) throws IOException {
RecordedRequest request = readRequest(stream);
requestQueue.add(request);
MockResponse response;
@@ -888,8 +894,14 @@ public final class MockWebServer implements TestRule {
path = value;
} else if (name.equals(Header.VERSION)) {
version = value;
- } else {
+ } else if (protocol == Protocol.SPDY_3) {
+ for (String s : value.split("\u0000", -1)) {
+ httpHeaders.add(name.utf8(), s);
+ }
+ } else if (protocol == Protocol.HTTP_2) {
httpHeaders.add(name.utf8(), value);
+ } else {
+ throw new IllegalStateException();
}
}
@@ -904,6 +916,11 @@ public final class MockWebServer implements TestRule {
}
private void writeResponse(FramedStream stream, MockResponse response) throws IOException {
+ Settings settings = response.getSettings();
+ if (settings != null) {
+ stream.getConnection().setSettings(settings);
+ }
+
if (response.getSocketPolicy() == SocketPolicy.NO_RESPONSE) {
return;
}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
index c9c206c..bb36cdf 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/QueueDispatcher.java
@@ -18,12 +18,14 @@ package com.squareup.okhttp.mockwebserver;
import java.net.HttpURLConnection;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
+import java.util.logging.Logger;
/**
* Default dispatcher that processes a script of responses. Populate the script
* by calling {@link #enqueueResponse(MockResponse)}.
*/
public class QueueDispatcher extends Dispatcher {
+ private static final Logger logger = Logger.getLogger(QueueDispatcher.class.getName());
protected final BlockingQueue<MockResponse> responseQueue = new LinkedBlockingQueue<>();
private MockResponse failFastResponse;
@@ -31,7 +33,7 @@ public class QueueDispatcher extends Dispatcher {
// To permit interactive/browser testing, ignore requests for favicons.
final String requestLine = request.getRequestLine();
if (requestLine != null && requestLine.equals("GET /favicon.ico HTTP/1.1")) {
- System.out.println("served " + requestLine);
+ logger.info("served " + requestLine);
return new MockResponse().setResponseCode(HttpURLConnection.HTTP_NOT_FOUND);
}
diff --git a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
index 4583621..f71f33d 100644
--- a/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
+++ b/mockwebserver/src/main/java/com/squareup/okhttp/mockwebserver/SocketPolicy.java
@@ -50,6 +50,9 @@ public enum SocketPolicy {
*/
DISCONNECT_AFTER_REQUEST,
+ /** Close connection after reading half of the request body (if present). */
+ DISCONNECT_DURING_REQUEST_BODY,
+
/** Close connection after writing half of the response body (if present). */
DISCONNECT_DURING_RESPONSE_BODY,
@@ -69,7 +72,7 @@ public enum SocketPolicy {
SHUTDOWN_OUTPUT_AT_END,
/**
- * Don't response to the request but keep the socket open. For testing
+ * Don't respond to the request but keep the socket open. For testing
* read response header timeout issue.
*/
NO_RESPONSE
diff --git a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
index 0f757dd..95e0fe4 100644
--- a/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
+++ b/mockwebserver/src/test/java/com/squareup/okhttp/mockwebserver/MockWebServerTest.java
@@ -20,6 +20,7 @@ import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.OutputStream;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
@@ -40,6 +41,7 @@ import org.junit.runners.model.Statement;
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.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -253,7 +255,30 @@ public final class MockWebServerTest {
in.close();
}
- @Test public void disconnectHalfway() throws IOException {
+ @Test public void disconnectRequestHalfway() throws IOException {
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_DURING_REQUEST_BODY));
+
+ HttpURLConnection connection = (HttpURLConnection) server.getUrl("/").openConnection();
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(true);
+ connection.setFixedLengthStreamingMode(1024 * 1024 * 1024); // 1 GB
+ connection.connect();
+ OutputStream out = connection.getOutputStream();
+
+ byte[] data = new byte[1024 * 1024];
+ int i;
+ for (i = 0; i < 1024; i++) {
+ try {
+ out.write(data);
+ out.flush();
+ } catch (IOException e) {
+ break;
+ }
+ }
+ assertEquals(512f, i, 10f); // Halfway +/- 1%
+ }
+
+ @Test public void disconnectResponseHalfway() throws IOException {
server.enqueue(new MockResponse()
.setBody("ab")
.setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
@@ -333,10 +358,4 @@ public final class MockWebServerTest {
} catch (ConnectException expected) {
}
}
-
- // ANDROID-BEGIN Android uses JUnit 4.10 which does not have assertNotEquals()
- private static void assertNotEquals(Object o1, Object o2) {
- org.junit.Assert.assertFalse(o1 == o2 || (o1 != null && o1.equals(o2)));
- }
- // ANDROID-END
}
diff --git a/okcurl/pom.xml b/okcurl/pom.xml
index e80d121..fa585b8 100644
--- a/okcurl/pom.xml
+++ b/okcurl/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okcurl</artifactId>
diff --git a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
index dbc51f3..65c2cd0 100644
--- a/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
+++ b/okcurl/src/main/java/com/squareup/okhttp/curl/Main.java
@@ -203,7 +203,7 @@ public class Main extends HelpOption implements Runnable {
}
String bodyData = data;
- String mimeType = "application/x-form-urlencoded";
+ String mimeType = "application/x-www-form-urlencoded";
if (headers != null) {
for (String header : headers) {
String[] parts = header.split(":", -1);
diff --git a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
index 0e2e3ae..d6961e1 100644
--- a/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
+++ b/okcurl/src/test/java/com/squareup/okhttp/curl/MainTest.java
@@ -45,7 +45,7 @@ public class MainTest {
RequestBody body = request.body();
assertEquals("POST", request.method());
assertEquals("http://example.com/", request.urlString());
- assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
+ assertEquals("application/x-www-form-urlencoded; charset=utf-8", body.contentType().toString());
assertEquals("foo", bodyAsString(body));
}
@@ -54,7 +54,7 @@ public class MainTest {
RequestBody body = request.body();
assertEquals("PUT", request.method());
assertEquals("http://example.com/", request.urlString());
- assertEquals("application/x-form-urlencoded; charset=utf-8", body.contentType().toString());
+ assertEquals("application/x-www-form-urlencoded; charset=utf-8", body.contentType().toString());
assertEquals("foo", bodyAsString(body));
}
diff --git a/okhttp-android-support/pom.xml b/okhttp-android-support/pom.xml
index f514808..0cb0f7e 100644
--- a/okhttp-android-support/pom.xml
+++ b/okhttp-android-support/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-android-support</artifactId>
diff --git a/okhttp-apache/pom.xml b/okhttp-apache/pom.xml
index 74ff837..4da1a4e 100644
--- a/okhttp-apache/pom.xml
+++ b/okhttp-apache/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-apache</artifactId>
diff --git a/okhttp-hpacktests/src/test/resources/hpack-test-case b/okhttp-hpacktests/src/test/resources/hpack-test-case
new file mode 160000
+Subproject a5652bc2bc3d2a992f39446369fb004a72e881d
diff --git a/okhttp-logging-interceptor/README.md b/okhttp-logging-interceptor/README.md
new file mode 100644
index 0000000..a16bbd2
--- /dev/null
+++ b/okhttp-logging-interceptor/README.md
@@ -0,0 +1,49 @@
+Logging Interceptor
+===================
+
+An [OkHttp interceptor][1] which logs HTTP request and response data.
+
+```java
+OkHttpClient client = new OkHttpClient();
+HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
+logging.setLevel(Level.BASIC);
+client.interceptors().add(logging);
+```
+
+You can change the log level at any time by calling `setLevel`.
+
+To log to a custom location, pass a `Logger` instance to the constructor.
+```java
+HttpLoggingInterceptor logging = new HttpLoggingInterceptor(new Logger() {
+ @Override public void log(String message) {
+ Timber.tag("OkHttp").d(message);
+ }
+});
+```
+
+**Warning**: The logs generated by this interceptor when using the `HEADERS` or `BODY` levels has
+the potential to leak sensitive information such as "Authorization" or "Cookie" headers and the
+contents of request and response bodies. This data should only be logged in a controlled way or in
+a non-production environment.
+
+
+Download
+--------
+
+Get via Maven:
+```xml
+<dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>logging-interceptor</artifactId>
+ <version>(insert latest version)</version>
+</dependency>
+```
+
+or via Gradle
+```groovy
+compile 'com.squareup.okhttp:logging-interceptor:(insert latest version)'
+```
+
+
+
+ [1]: https://github.com/square/okhttp/wiki/Interceptors
diff --git a/okhttp-logging-interceptor/pom.xml b/okhttp-logging-interceptor/pom.xml
new file mode 100644
index 0000000..efd552c
--- /dev/null
+++ b/okhttp-logging-interceptor/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>parent</artifactId>
+ <version>2.7.5</version>
+ </parent>
+
+ <artifactId>logging-interceptor</artifactId>
+ <name>OkHttp Logging Interceptor</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>mockwebserver</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.squareup.okhttp</groupId>
+ <artifactId>okhttp-testing-support</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java b/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java
new file mode 100644
index 0000000..402eee0
--- /dev/null
+++ b/okhttp-logging-interceptor/src/main/java/com/squareup/okhttp/logging/HttpLoggingInterceptor.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.logging;
+
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.http.HttpEngine;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.concurrent.TimeUnit;
+import okio.Buffer;
+import okio.BufferedSource;
+
+/**
+ * An OkHttp interceptor which logs request and response information. Can be applied as an
+ * {@linkplain OkHttpClient#interceptors() application interceptor} or as a
+ * {@linkplain OkHttpClient#networkInterceptors() network interceptor}.
+ * <p>
+ * The format of the logs created by this class should not be considered stable and may change
+ * slightly between releases. If you need a stable logging format, use your own interceptor.
+ */
+public final class HttpLoggingInterceptor implements Interceptor {
+ private static final Charset UTF8 = Charset.forName("UTF-8");
+
+ public enum Level {
+ /** No logs. */
+ NONE,
+ /**
+ * Logs request and response lines.
+ * <p>
+ * Example:
+ * <pre>{@code
+ * --> POST /greeting HTTP/1.1 (3-byte body)
+ *
+ * <-- HTTP/1.1 200 OK (22ms, 6-byte body)
+ * }</pre>
+ */
+ BASIC,
+ /**
+ * Logs request and response lines and their respective headers.
+ * <p>
+ * Example:
+ * <pre>{@code
+ * --> POST /greeting HTTP/1.1
+ * Host: example.com
+ * Content-Type: plain/text
+ * Content-Length: 3
+ * --> END POST
+ *
+ * <-- HTTP/1.1 200 OK (22ms)
+ * Content-Type: plain/text
+ * Content-Length: 6
+ * <-- END HTTP
+ * }</pre>
+ */
+ HEADERS,
+ /**
+ * Logs request and response lines and their respective headers and bodies (if present).
+ * <p>
+ * Example:
+ * <pre>{@code
+ * --> POST /greeting HTTP/1.1
+ * Host: example.com
+ * Content-Type: plain/text
+ * Content-Length: 3
+ *
+ * Hi?
+ * --> END GET
+ *
+ * <-- HTTP/1.1 200 OK (22ms)
+ * Content-Type: plain/text
+ * Content-Length: 6
+ *
+ * Hello!
+ * <-- END HTTP
+ * }</pre>
+ */
+ BODY
+ }
+
+ public interface Logger {
+ void log(String message);
+
+ /** A {@link Logger} defaults output appropriate for the current platform. */
+ Logger DEFAULT = new Logger() {
+ @Override public void log(String message) {
+ Platform.get().log(message);
+ }
+ };
+ }
+
+ public HttpLoggingInterceptor() {
+ this(Logger.DEFAULT);
+ }
+
+ public HttpLoggingInterceptor(Logger logger) {
+ this.logger = logger;
+ }
+
+ private final Logger logger;
+
+ private volatile Level level = Level.NONE;
+
+ /** Change the level at which this interceptor logs. */
+ public HttpLoggingInterceptor setLevel(Level level) {
+ if (level == null) throw new NullPointerException("level == null. Use Level.NONE instead.");
+ this.level = level;
+ return this;
+ }
+
+ public Level getLevel() {
+ return level;
+ }
+
+ @Override public Response intercept(Chain chain) throws IOException {
+ Level level = this.level;
+
+ Request request = chain.request();
+ if (level == Level.NONE) {
+ return chain.proceed(request);
+ }
+
+ boolean logBody = level == Level.BODY;
+ boolean logHeaders = logBody || level == Level.HEADERS;
+
+ RequestBody requestBody = request.body();
+ boolean hasRequestBody = requestBody != null;
+
+ Connection connection = chain.connection();
+ Protocol protocol = connection != null ? connection.getProtocol() : Protocol.HTTP_1_1;
+ String requestStartMessage =
+ "--> " + request.method() + ' ' + request.httpUrl() + ' ' + protocol(protocol);
+ if (!logHeaders && hasRequestBody) {
+ requestStartMessage += " (" + requestBody.contentLength() + "-byte body)";
+ }
+ logger.log(requestStartMessage);
+
+ if (logHeaders) {
+ if (hasRequestBody) {
+ // Request body headers are only present when installed as a network interceptor. Force
+ // them to be included (when available) so there values are known.
+ if (requestBody.contentType() != null) {
+ logger.log("Content-Type: " + requestBody.contentType());
+ }
+ if (requestBody.contentLength() != -1) {
+ logger.log("Content-Length: " + requestBody.contentLength());
+ }
+ }
+
+ Headers headers = request.headers();
+ for (int i = 0, count = headers.size(); i < count; i++) {
+ String name = headers.name(i);
+ // Skip headers from the request body as they are explicitly logged above.
+ if (!"Content-Type".equalsIgnoreCase(name) && !"Content-Length".equalsIgnoreCase(name)) {
+ logger.log(name + ": " + headers.value(i));
+ }
+ }
+
+ if (!logBody || !hasRequestBody) {
+ logger.log("--> END " + request.method());
+ } else if (bodyEncoded(request.headers())) {
+ logger.log("--> END " + request.method() + " (encoded body omitted)");
+ } else {
+ Buffer buffer = new Buffer();
+ requestBody.writeTo(buffer);
+
+ Charset charset = UTF8;
+ MediaType contentType = requestBody.contentType();
+ if (contentType != null) {
+ contentType.charset(UTF8);
+ }
+
+ logger.log("");
+ logger.log(buffer.readString(charset));
+
+ logger.log("--> END " + request.method()
+ + " (" + requestBody.contentLength() + "-byte body)");
+ }
+ }
+
+ long startNs = System.nanoTime();
+ Response response = chain.proceed(request);
+ long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs);
+
+ ResponseBody responseBody = response.body();
+ logger.log("<-- " + protocol(response.protocol()) + ' ' + response.code() + ' '
+ + response.message() + " (" + tookMs + "ms"
+ + (!logHeaders ? ", " + responseBody.contentLength() + "-byte body" : "") + ')');
+
+ if (logHeaders) {
+ Headers headers = response.headers();
+ for (int i = 0, count = headers.size(); i < count; i++) {
+ logger.log(headers.name(i) + ": " + headers.value(i));
+ }
+
+ if (!logBody || !HttpEngine.hasBody(response)) {
+ logger.log("<-- END HTTP");
+ } else if (bodyEncoded(response.headers())) {
+ logger.log("<-- END HTTP (encoded body omitted)");
+ } else {
+ BufferedSource source = responseBody.source();
+ source.request(Long.MAX_VALUE); // Buffer the entire body.
+ Buffer buffer = source.buffer();
+
+ Charset charset = UTF8;
+ MediaType contentType = responseBody.contentType();
+ if (contentType != null) {
+ charset = contentType.charset(UTF8);
+ }
+
+ if (responseBody.contentLength() != 0) {
+ logger.log("");
+ logger.log(buffer.clone().readString(charset));
+ }
+
+ logger.log("<-- END HTTP (" + buffer.size() + "-byte body)");
+ }
+ }
+
+ return response;
+ }
+
+ private boolean bodyEncoded(Headers headers) {
+ String contentEncoding = headers.get("Content-Encoding");
+ return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity");
+ }
+
+ private static String protocol(Protocol protocol) {
+ return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
+ }
+}
diff --git a/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java b/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java
new file mode 100644
index 0000000..dbd1e84
--- /dev/null
+++ b/okhttp-logging-interceptor/src/test/java/com/squareup/okhttp/logging/HttpLoggingInterceptorTest.java
@@ -0,0 +1,610 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.logging;
+
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.logging.HttpLoggingInterceptor.Level;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Pattern;
+import okio.Buffer;
+import okio.BufferedSink;
+import okio.ByteString;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class HttpLoggingInterceptorTest {
+ private static final MediaType PLAIN = MediaType.parse("text/plain; charset=utf-8");
+
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private final OkHttpClient client = new OkHttpClient();
+ private String host;
+ private HttpUrl url;
+
+ private final LogRecorder networkLogs = new LogRecorder();
+ private final HttpLoggingInterceptor networkInterceptor =
+ new HttpLoggingInterceptor(networkLogs);
+
+ private final LogRecorder applicationLogs = new LogRecorder();
+ private final HttpLoggingInterceptor applicationInterceptor =
+ new HttpLoggingInterceptor(applicationLogs);
+
+ private void setLevel(Level level) {
+ networkInterceptor.setLevel(level);
+ applicationInterceptor.setLevel(level);
+ }
+
+ @Before public void setUp() {
+ client.networkInterceptors().add(networkInterceptor);
+ client.interceptors().add(applicationInterceptor);
+ client.setConnectionPool(null);
+
+ host = server.getHostName() + ":" + server.getPort();
+ url = server.url("/");
+ }
+
+ @Test public void levelGetter() {
+ // The default is NONE.
+ assertEquals(Level.NONE, applicationInterceptor.getLevel());
+
+ for (Level level : Level.values()) {
+ applicationInterceptor.setLevel(level);
+ assertEquals(level, applicationInterceptor.getLevel());
+ }
+ }
+
+ @Test public void setLevelShouldPreventNullValue() {
+ try {
+ applicationInterceptor.setLevel(null);
+ fail();
+ } catch (NullPointerException expected) {
+ assertEquals("level == null. Use Level.NONE instead.", expected.getMessage());
+ }
+ }
+
+ @Test public void setLevelShouldReturnSameInstanceOfInterceptor() {
+ for (Level level : Level.values()) {
+ assertSame(applicationInterceptor, applicationInterceptor.setLevel(level));
+ }
+ }
+
+ @Test public void none() throws IOException {
+ server.enqueue(new MockResponse());
+ client.newCall(request().build()).execute();
+
+ applicationLogs.assertNoMoreLogs();
+ networkLogs.assertNoMoreLogs();
+ }
+
+ @Test public void basicGet() throws IOException {
+ setLevel(Level.BASIC);
+
+ server.enqueue(new MockResponse());
+ client.newCall(request().build()).execute();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void basicPost() throws IOException {
+ setLevel(Level.BASIC);
+
+ server.enqueue(new MockResponse());
+ client.newCall(request().post(RequestBody.create(PLAIN, "Hi?")).build()).execute();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1 (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1 (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 0-byte body\\)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void basicResponseBody() throws IOException {
+ setLevel(Level.BASIC);
+
+ server.enqueue(new MockResponse()
+ .setBody("Hello!")
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 6-byte body\\)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms, 6-byte body\\)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersGet() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersPost() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ Request request = request().post(RequestBody.create(PLAIN, "Hi?")).build();
+ Response response = client.newCall(request).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersPostNoContentType() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ Request request = request().post(RequestBody.create(null, "Hi?")).build();
+ Response response = client.newCall(request).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersPostNoLength() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse());
+ RequestBody body = new RequestBody() {
+ @Override public MediaType contentType() {
+ return PLAIN;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Hi!");
+ }
+ };
+ Response response = client.newCall(request().post(body).build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Transfer-Encoding: chunked")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END POST")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void headersResponseBody() throws IOException {
+ setLevel(Level.HEADERS);
+
+ server.enqueue(new MockResponse()
+ .setBody("Hello!")
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyGet() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse());
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyGet204() throws IOException {
+ setLevel(Level.BODY);
+ bodyGetNoBody(204);
+ }
+
+ @Test public void bodyGet205() throws IOException {
+ setLevel(Level.BODY);
+ bodyGetNoBody(205);
+ }
+
+ private void bodyGetNoBody(int code) throws IOException {
+ server.enqueue(new MockResponse()
+ .setStatus("HTTP/1.1 " + code + " No Content"));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 " + code + " No Content \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 " + code + " No Content \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyPost() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse());
+ Request request = request().post(RequestBody.create(PLAIN, "Hi?")).build();
+ Response response = client.newCall(request).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("")
+ .assertLogEqual("Hi?")
+ .assertLogEqual("--> END POST (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> POST " + url + " HTTP/1.1")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogEqual("Content-Length: 3")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("")
+ .assertLogEqual("Hi?")
+ .assertLogEqual("--> END POST (3-byte body)")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 0")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (0-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyResponseBody() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse()
+ .setBody("Hello!")
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Length: 6")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyResponseBodyChunked() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse()
+ .setChunkedBody("Hello!", 2)
+ .setHeader("Content-Type", PLAIN));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Transfer-encoding: chunked")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Transfer-encoding: chunked")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello!")
+ .assertLogEqual("<-- END HTTP (6-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ @Test public void bodyResponseNotIdentityEncoded() throws IOException {
+ setLevel(Level.BODY);
+
+ server.enqueue(new MockResponse()
+ .setHeader("Content-Encoding", "gzip")
+ .setHeader("Content-Type", PLAIN)
+ .setBody(new Buffer().write(ByteString.decodeBase64(
+ "H4sIAAAAAAAAAPNIzcnJ11HwQKIAdyO+9hMAAAA="))));
+ Response response = client.newCall(request().build()).execute();
+ response.body().close();
+
+ networkLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("Host: " + host)
+ .assertLogEqual("Connection: Keep-Alive")
+ .assertLogEqual("Accept-Encoding: gzip")
+ .assertLogMatch("User-Agent: okhttp/.+")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Encoding: gzip")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("Content-Length: \\d+")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("<-- END HTTP (encoded body omitted)")
+ .assertNoMoreLogs();
+
+ applicationLogs
+ .assertLogEqual("--> GET " + url + " HTTP/1.1")
+ .assertLogEqual("--> END GET")
+ .assertLogMatch("<-- HTTP/1\\.1 200 OK \\(\\d+ms\\)")
+ .assertLogEqual("Content-Type: text/plain; charset=utf-8")
+ .assertLogMatch("OkHttp-Sent-Millis: \\d+")
+ .assertLogMatch("OkHttp-Received-Millis: \\d+")
+ .assertLogEqual("")
+ .assertLogEqual("Hello, Hello, Hello")
+ .assertLogEqual("<-- END HTTP (19-byte body)")
+ .assertNoMoreLogs();
+ }
+
+ private Request.Builder request() {
+ return new Request.Builder().url(url);
+ }
+
+ private static class LogRecorder implements HttpLoggingInterceptor.Logger {
+ private final List<String> logs = new ArrayList<>();
+ private int index;
+
+ LogRecorder assertLogEqual(String expected) {
+ assertTrue("No more messages found", index < logs.size());
+ String actual = logs.get(index++);
+ assertEquals(expected, actual);
+ return this;
+ }
+
+ LogRecorder assertLogMatch(String pattern) {
+ assertTrue("No more messages found", index < logs.size());
+ String actual = logs.get(index++);
+ assertTrue("<" + actual + "> did not match pattern <" + pattern + ">",
+ Pattern.matches(pattern, actual));
+ return this;
+ }
+
+ void assertNoMoreLogs() {
+ assertTrue("More messages remain: " + logs.subList(index, logs.size()), index == logs.size());
+ }
+
+ @Override public void log(String message) {
+ logs.add(message);
+ }
+ }
+}
diff --git a/okhttp-testing-support/pom.xml b/okhttp-testing-support/pom.xml
index 654b0e3..42b4701 100644
--- a/okhttp-testing-support/pom.xml
+++ b/okhttp-testing-support/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-testing-support</artifactId>
diff --git a/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
index 3a043cb..6498fe8 100644
--- a/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
+++ b/okhttp-testing-support/src/main/java/com/squareup/okhttp/internal/io/InMemoryFileSystem.java
@@ -18,32 +18,95 @@ package com.squareup.okhttp.internal.io;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import okio.Buffer;
+import okio.ForwardingSink;
+import okio.ForwardingSource;
import okio.Sink;
import okio.Source;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
/** A simple file system where all files are held in memory. Not safe for concurrent use. */
-public final class InMemoryFileSystem implements FileSystem {
+public final class InMemoryFileSystem implements FileSystem, TestRule {
private final Map<File, Buffer> files = new LinkedHashMap<>();
+ private final Map<Source, File> openSources = new IdentityHashMap<>();
+ private final Map<Sink, File> openSinks = new IdentityHashMap<>();
+
+ @Override public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override public void evaluate() throws Throwable {
+ base.evaluate();
+ ensureResourcesClosed();
+ }
+ };
+ }
+
+ public void ensureResourcesClosed() {
+ List<String> openResources = new ArrayList<>();
+ for (File file : openSources.values()) {
+ openResources.add("Source for " + file);
+ }
+ for (File file : openSinks.values()) {
+ openResources.add("Sink for " + file);
+ }
+ if (!openResources.isEmpty()) {
+ StringBuilder builder = new StringBuilder("Resources acquired but not closed:");
+ for (String resource : openResources) {
+ builder.append("\n * ").append(resource);
+ }
+ throw new IllegalStateException(builder.toString());
+ }
+ }
@Override public Source source(File file) throws FileNotFoundException {
Buffer result = files.get(file);
if (result == null) throw new FileNotFoundException();
- return result.clone();
+
+ final Source source = result.clone();
+ openSources.put(source, file);
+
+ return new ForwardingSource(source) {
+ @Override public void close() throws IOException {
+ openSources.remove(source);
+ super.close();
+ }
+ };
}
@Override public Sink sink(File file) throws FileNotFoundException {
- Buffer result = new Buffer();
- files.put(file, result);
- return result;
+ return sink(file, false);
}
@Override public Sink appendingSink(File file) throws FileNotFoundException {
- Buffer result = files.get(file);
- return result != null ? result : sink(file);
+ return sink(file, true);
+ }
+
+ private Sink sink(File file, boolean appending) {
+ Buffer result = null;
+ if (appending) {
+ result = files.get(file);
+ }
+ if (result == null) {
+ result = new Buffer();
+ }
+ files.put(file, result);
+
+ final Sink sink = result;
+ openSinks.put(sink, file);
+
+ return new ForwardingSink(sink) {
+ @Override public void close() throws IOException {
+ openSinks.remove(sink);
+ super.close();
+ }
+ };
}
@Override public void delete(File file) throws IOException {
diff --git a/okhttp-tests/pom.xml b/okhttp-tests/pom.xml
index 2bb1982..911afd2 100644
--- a/okhttp-tests/pom.xml
+++ b/okhttp-tests/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-tests</artifactId>
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
index 44c39a8..1e623f0 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AddressTest.java
@@ -26,6 +26,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public final class AddressTest {
+ private Dns dns = Dns.SYSTEM;
private SocketFactory socketFactory = SocketFactory.getDefault();
private Authenticator authenticator = AuthenticatorAdapter.INSTANCE;
private List<Protocol> protocols = Util.immutableList(Protocol.HTTP_1_1);
@@ -33,18 +34,18 @@ public final class AddressTest {
private RecordingProxySelector proxySelector = new RecordingProxySelector();
@Test public void equalsAndHashcode() throws Exception {
- Address a = new Address("square.com", 80, socketFactory, null, null, null,
+ Address a = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, proxySelector);
- Address b = new Address("square.com", 80, socketFactory, null, null, null,
+ Address b = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, proxySelector);
assertEquals(a, b);
assertEquals(a.hashCode(), b.hashCode());
}
@Test public void differentProxySelectorsAreDifferent() throws Exception {
- Address a = new Address("square.com", 80, socketFactory, null, null, null,
+ Address a = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
- Address b = new Address("square.com", 80, socketFactory, null, null, null,
+ Address b = new Address("square.com", 80, dns, socketFactory, null, null, null,
authenticator, null, protocols, connectionSpecs, new RecordingProxySelector());
assertFalse(a.equals(b));
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
index b9e1d50..c762862 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CacheTest.java
@@ -19,7 +19,6 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.io.FileSystem;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -76,9 +75,9 @@ public final class CacheTest {
@Rule public MockWebServer server = new MockWebServer();
@Rule public MockWebServer server2 = new MockWebServer();
+ @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
private final SSLContext sslContext = SslContextBuilder.localhost();
- private final FileSystem fileSystem = new InMemoryFileSystem();
private final OkHttpClient client = new OkHttpClient();
private Cache cache;
private final CookieManager cookieManager = new CookieManager();
@@ -93,6 +92,7 @@ public final class CacheTest {
@After public void tearDown() throws Exception {
ResponseCache.setDefault(null);
CookieHandler.setDefault(null);
+ cache.delete();
}
/**
@@ -266,7 +266,7 @@ public final class CacheTest {
Principal localPrincipal = response1.handshake().localPrincipal();
Response response2 = client.newCall(request).execute(); // Cached!
- assertEquals("ABC", response2.body().source().readUtf8());
+ assertEquals("ABC", response2.body().string());
assertEquals(2, cache.getRequestCount());
assertEquals(1, cache.getNetworkCount());
@@ -462,6 +462,26 @@ public final class CacheTest {
assertEquals("b", get(url).body().string());
}
+ /** https://github.com/square/okhttp/issues/2198 */
+ @Test public void cachedRedirect() throws IOException {
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Cache-Control: max-age=60")
+ .addHeader("Location: /bar"));
+ server.enqueue(new MockResponse()
+ .setBody("ABC"));
+ server.enqueue(new MockResponse()
+ .setBody("ABC"));
+
+ Request request1 = new Request.Builder().url(server.url("/")).build();
+ Response response1 = client.newCall(request1).execute();
+ assertEquals("ABC", response1.body().string());
+
+ Request request2 = new Request.Builder().url(server.url("/")).build();
+ Response response2 = client.newCall(request2).execute();
+ assertEquals("ABC", response2.body().string());
+ }
+
@Test public void serverDisconnectsPrematurelyWithContentLengthHeader() throws IOException {
testServerPrematureDisconnect(TransferKind.FIXED_LENGTH);
}
@@ -1007,7 +1027,7 @@ public final class CacheTest {
assertEquals("A", get(server.url("/")).body().string());
assertEquals("A", get(server.url("/")).body().string());
- assertEquals(1, client.getConnectionPool().getConnectionCount());
+ assertEquals(1, client.getConnectionPool().getIdleConnectionCount());
}
@Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -1873,6 +1893,7 @@ public final class CacheTest {
Response response = get(server.url("/"));
assertEquals("A", response.header(""));
+ assertEquals("body", response.body().string());
}
/**
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
index 051eae4..34ca42a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CallTest.java
@@ -15,14 +15,13 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.DoubleInetAddressNetwork;
-import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.DoubleInetAddressDns;
import com.squareup.okhttp.internal.RecordingOkAuthenticator;
-import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SingleInetAddressDns;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.Version;
-import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.http.FakeDns;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.Dispatcher;
import com.squareup.okhttp.mockwebserver.MockResponse;
@@ -88,9 +87,9 @@ public final class CallTest {
@Rule public final TestRule timeout = new Timeout(30_000);
@Rule public final MockWebServer server = new MockWebServer();
@Rule public final MockWebServer server2 = new MockWebServer();
+ @Rule public final InMemoryFileSystem fileSystem = new InMemoryFileSystem();
private SSLContext sslContext = SslContextBuilder.localhost();
- private FileSystem fileSystem = new InMemoryFileSystem();
private OkHttpClient client = new OkHttpClient();
private RecordingCallback callback = new RecordingCallback();
private TestLogHandler logHandler = new TestLogHandler();
@@ -172,14 +171,48 @@ public final class CallTest {
.assertNotSuccessful();
}
+ @Test public void get_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+ get();
+ }
+
+ @Test public void get_HTTPS() throws Exception {
+ enableTls();
+ get();
+ }
+
@Test public void get_SPDY_3() throws Exception {
enableProtocol(Protocol.SPDY_3);
get();
}
- @Test public void get_HTTP_2() throws Exception {
+ @Test public void repeatedHeaderNames() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("B", "123")
+ .addHeader("B", "234"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .addHeader("A", "345")
+ .addHeader("A", "456")
+ .build();
+
+ executeSynchronously(request)
+ .assertCode(200)
+ .assertHeader("B", "123", "234");
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals(Arrays.asList("345", "456"), recordedRequest.getHeaders().values("A"));
+ }
+
+ @Test public void repeatedHeaderNames_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ repeatedHeaderNames();
+ }
+
+ @Test public void repeatedHeaderNames_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
- get();
+ repeatedHeaderNames();
}
@Test public void getWithRequestBody() throws Exception {
@@ -212,8 +245,8 @@ public final class CallTest {
assertNull(recordedRequest.getHeader("Content-Length"));
}
- @Test public void head_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void head_HTTPS() throws Exception {
+ enableTls();
head();
}
@@ -222,6 +255,11 @@ public final class CallTest {
head();
}
+ @Test public void head_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ head();
+ }
+
@Test public void post() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -241,8 +279,8 @@ public final class CallTest {
assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
}
- @Test public void post_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void post_HTTPS() throws Exception {
+ enableTls();
post();
}
@@ -251,6 +289,11 @@ public final class CallTest {
post();
}
+ @Test public void post_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ post();
+ }
+
@Test public void postZeroLength() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -270,8 +313,8 @@ public final class CallTest {
assertEquals(null, recordedRequest.getHeader("Content-Type"));
}
- @Test public void postZeroLength_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void postZerolength_HTTPS() throws Exception {
+ enableTls();
postZeroLength();
}
@@ -280,12 +323,17 @@ public final class CallTest {
postZeroLength();
}
+ @Test public void postZeroLength_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postZeroLength();
+ }
+
@Test public void postBodyRetransmittedAfterAuthorizationFail() throws Exception {
postBodyRetransmittedAfterAuthorizationFail("abc");
}
- @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception {
+ enableTls();
postBodyRetransmittedAfterAuthorizationFail("abc");
}
@@ -294,13 +342,18 @@ public final class CallTest {
postBodyRetransmittedAfterAuthorizationFail("abc");
}
+ @Test public void postBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postBodyRetransmittedAfterAuthorizationFail("abc");
+ }
+
/** Don't explode when resending an empty post. https://github.com/square/okhttp/issues/1131 */
@Test public void postEmptyBodyRetransmittedAfterAuthorizationFail() throws Exception {
postBodyRetransmittedAfterAuthorizationFail("");
}
- @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_HTTPS() throws Exception {
+ enableTls();
postBodyRetransmittedAfterAuthorizationFail("");
}
@@ -309,6 +362,11 @@ public final class CallTest {
postBodyRetransmittedAfterAuthorizationFail("");
}
+ @Test public void postEmptyBodyRetransmittedAfterAuthorizationFail_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ postBodyRetransmittedAfterAuthorizationFail("");
+ }
+
private void postBodyRetransmittedAfterAuthorizationFail(String body) throws Exception {
server.enqueue(new MockResponse().setResponseCode(401));
server.enqueue(new MockResponse());
@@ -385,8 +443,8 @@ public final class CallTest {
assertEquals(null, recordedRequest.getHeader("Content-Type"));
}
- @Test public void delete_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void delete_HTTPS() throws Exception {
+ enableTls();
delete();
}
@@ -395,6 +453,11 @@ public final class CallTest {
delete();
}
+ @Test public void delete_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ delete();
+ }
+
@Test public void deleteWithRequestBody() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -431,8 +494,8 @@ public final class CallTest {
assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
}
- @Test public void put_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void put_HTTPS() throws Exception {
+ enableTls();
put();
}
@@ -441,6 +504,11 @@ public final class CallTest {
put();
}
+ @Test public void put_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
+ put();
+ }
+
@Test public void patch() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
@@ -460,13 +528,18 @@ public final class CallTest {
assertEquals("text/plain; charset=utf-8", recordedRequest.getHeader("Content-Type"));
}
- @Test public void patch_SPDY_3() throws Exception {
- enableProtocol(Protocol.SPDY_3);
+ @Test public void patch_HTTP_2() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
patch();
}
- @Test public void patch_HTTP_2() throws Exception {
- enableProtocol(Protocol.HTTP_2);
+ @Test public void patch_HTTPS() throws Exception {
+ enableTls();
+ patch();
+ }
+
+ @Test public void patch_SPDY_3() throws Exception {
+ enableProtocol(Protocol.SPDY_3);
patch();
}
@@ -497,7 +570,8 @@ public final class CallTest {
.build();
Call call = client.newCall(request);
- call.execute();
+ Response response = call.execute();
+ response.body().close();
try {
call.execute();
@@ -675,17 +749,19 @@ public final class CallTest {
long elapsedNanos = System.nanoTime() - startNanos;
long elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedNanos);
assertTrue(String.format("Timed out: %sms", elapsedMillis), elapsedMillis < 500);
+ } finally {
+ bodySource.close();
}
}
- // https://github.com/square/okhttp/issues/442
+ /** https://github.com/square/okhttp/issues/442 */
@Test public void timeoutsNotRetried() throws Exception {
server.enqueue(new MockResponse()
.setSocketPolicy(SocketPolicy.NO_RESPONSE));
server.enqueue(new MockResponse()
.setBody("unreachable!"));
- Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setDns(new DoubleInetAddressDns());
client.setReadTimeout(100, TimeUnit.MILLISECONDS);
Request request = new Request.Builder().url(server.url("/")).build();
@@ -697,6 +773,20 @@ public final class CallTest {
}
}
+ /** https://github.com/square/okhttp/issues/1801 */
+ @Test public void asyncCallEngineInitialized() throws Exception {
+ OkHttpClient c = new OkHttpClient();
+ c.interceptors().add(new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ throw new IOException();
+ }
+ });
+ Request request = new Request.Builder().url(server.url("/")).build();
+ c.newCall(request).enqueue(callback);
+ RecordedResponse response = callback.await(request.httpUrl());
+ assertEquals(request, response.request);
+ }
+
@Test public void reusedSinksGetIndependentTimeoutInstances() throws Exception {
server.enqueue(new MockResponse());
server.enqueue(new MockResponse());
@@ -764,27 +854,21 @@ public final class CallTest {
}
@Test public void tls() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse()
.setBody("abc")
.addHeader("Content-Type: text/plain"));
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
executeSynchronously(new Request.Builder().url(server.url("/")).build())
.assertHandshake();
}
@Test public void tls_Async() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse()
.setBody("abc")
.addHeader("Content-Type: text/plain"));
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
Request request = new Request.Builder()
.url(server.url("/"))
.build();
@@ -794,25 +878,28 @@ public final class CallTest {
}
@Test public void recoverWhenRetryOnConnectionFailureIsTrue() throws Exception {
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse().setBody("seed connection pool"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
server.enqueue(new MockResponse().setBody("retry success"));
- Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setDns(new DoubleInetAddressDns());
assertTrue(client.getRetryOnConnectionFailure());
Request request = new Request.Builder().url(server.url("/")).build();
- Response response = client.newCall(request).execute();
- assertEquals("retry success", response.body().string());
+ executeSynchronously(request).assertBody("seed connection pool");
+ executeSynchronously(request).assertBody("retry success");
}
@Test public void noRecoverWhenRetryOnConnectionFailureIsFalse() throws Exception {
- server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AT_START));
+ server.enqueue(new MockResponse().setBody("seed connection pool"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
server.enqueue(new MockResponse().setBody("unreachable!"));
- Internal.instance.setNetwork(client, new DoubleInetAddressNetwork());
+ client.setDns(new DoubleInetAddressDns());
client.setRetryOnConnectionFailure(false);
Request request = new Request.Builder().url(server.url("/")).build();
+ executeSynchronously(request).assertBody("seed connection pool");
try {
// If this succeeds, too many requests were made.
client.newCall(request).execute();
@@ -828,7 +915,7 @@ public final class CallTest {
suppressTlsFallbackScsv(client);
client.setHostnameVerifier(new RecordingHostnameVerifier());
- Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+ client.setDns(new SingleInetAddressDns());
executeSynchronously(new Request.Builder().url(server.url("/")).build())
.assertBody("abc");
@@ -850,7 +937,7 @@ public final class CallTest {
new RecordingSSLSocketFactory(sslContext.getSocketFactory());
client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
- Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+ client.setDns(new SingleInetAddressDns());
Request request = new Request.Builder().url(server.url("/")).build();
try {
@@ -890,7 +977,7 @@ public final class CallTest {
suppressTlsFallbackScsv(client);
client.setHostnameVerifier(new RecordingHostnameVerifier());
- Internal.instance.setNetwork(client, new SingleInetAddressNetwork());
+ client.setDns(new SingleInetAddressDns());
Request request = new Request.Builder().url(server.url("/")).build();
try {
@@ -920,26 +1007,24 @@ public final class CallTest {
}
@Test public void setFollowSslRedirectsFalse() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
- server.enqueue(new MockResponse().setResponseCode(301).addHeader("Location: http://square.com"));
+ enableTls();
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: http://square.com"));
client.setFollowSslRedirects(false);
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
Request request = new Request.Builder().url(server.url("/")).build();
Response response = client.newCall(request).execute();
assertEquals(301, response.code());
+ response.body().close();
}
@Test public void matchingPinnedCertificate() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse());
server.enqueue(new MockResponse());
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
// Make a first request without certificate pinning. Use it to collect certificates to pin.
Request request1 = new Request.Builder().url(server.url("/")).build();
Response response1 = client.newCall(request1).execute();
@@ -947,21 +1032,20 @@ public final class CallTest {
for (Certificate certificate : response1.handshake().peerCertificates()) {
certificatePinnerBuilder.add(server.getHostName(), CertificatePinner.pin(certificate));
}
+ response1.body().close();
// Make another request with certificate pinning. It should complete normally.
client.setCertificatePinner(certificatePinnerBuilder.build());
Request request2 = new Request.Builder().url(server.url("/")).build();
Response response2 = client.newCall(request2).execute();
assertNotSame(response2.handshake(), response1.handshake());
+ response2.body().close();
}
@Test public void unmatchingPinnedCertificate() throws Exception {
- server.useHttps(sslContext.getSocketFactory(), false);
+ enableTls();
server.enqueue(new MockResponse());
- client.setSslSocketFactory(sslContext.getSocketFactory());
- client.setHostnameVerifier(new RecordingHostnameVerifier());
-
// Pin publicobject.com's cert.
client.setCertificatePinner(new CertificatePinner.Builder()
.add(server.getHostName(), "sha1/DmxUShsZuNiqPQsX2Oi9uv2sCnw=")
@@ -1293,6 +1377,27 @@ public final class CallTest {
assertEquals("GET /page2 HTTP/1.1", page2.getRequestLine());
}
+ @Test public void propfindRedirectsToPropfind() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(HttpURLConnection.HTTP_MOVED_TEMP)
+ .addHeader("Location: /page2")
+ .setBody("This page has moved!"));
+ server.enqueue(new MockResponse().setBody("Page 2"));
+
+ Response response = client.newCall(new Request.Builder()
+ .url(server.url("/page1"))
+ .method("PROPFIND", RequestBody.create(MediaType.parse("text/plain"), "Request Body"))
+ .build()).execute();
+ assertEquals("Page 2", response.body().string());
+
+ RecordedRequest page1 = server.takeRequest();
+ assertEquals("PROPFIND /page1 HTTP/1.1", page1.getRequestLine());
+ assertEquals("Request Body", page1.getBody().readUtf8());
+
+ RecordedRequest page2 = server.takeRequest();
+ assertEquals("PROPFIND /page2 HTTP/1.1", page2.getRequestLine());
+ }
+
@Test public void redirectsDoNotIncludeTooManyCookies() throws Exception {
server2.enqueue(new MockResponse().setBody("Page 2"));
server.enqueue(new MockResponse()
@@ -1511,13 +1616,13 @@ public final class CallTest {
}
@Test public void cancelTagImmediatelyAfterEnqueue() throws Exception {
+ server.enqueue(new MockResponse());
Call call = client.newCall(new Request.Builder()
.url(server.url("/a"))
.tag("request")
.build());
call.enqueue(callback);
client.cancel("request");
- assertEquals(0, server.getRequestCount());
callback.await(server.url("/a")).assertFailure("Canceled");
}
@@ -1559,6 +1664,11 @@ public final class CallTest {
}
}
+ @Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTPS() throws Exception {
+ enableTls();
+ cancelInFlightBeforeResponseReadThrowsIOE();
+ }
+
@Test public void cancelInFlightBeforeResponseReadThrowsIOE_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
cancelInFlightBeforeResponseReadThrowsIOE();
@@ -1596,6 +1706,11 @@ public final class CallTest {
callback.await(requestB.httpUrl()).assertFailure("Canceled");
}
+ @Test public void canceledBeforeIOSignalsOnFailure_HTTPS() throws Exception {
+ enableTls();
+ canceledBeforeIOSignalsOnFailure();
+ }
+
@Test public void canceledBeforeIOSignalsOnFailure_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
canceledBeforeIOSignalsOnFailure();
@@ -1623,6 +1738,11 @@ public final class CallTest {
"Socket closed");
}
+ @Test public void canceledBeforeResponseReadSignalsOnFailure_HTTPS() throws Exception {
+ enableTls();
+ canceledBeforeResponseReadSignalsOnFailure();
+ }
+
@Test public void canceledBeforeResponseReadSignalsOnFailure_HTTP_2() throws Exception {
enableProtocol(Protocol.HTTP_2);
canceledBeforeResponseReadSignalsOnFailure();
@@ -1670,6 +1790,12 @@ public final class CallTest {
assertFalse(failureRef.get());
}
+ @Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTPS()
+ throws Exception {
+ enableTls();
+ canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce();
+ }
+
@Test public void canceledAfterResponseIsDeliveredBreaksStreamButSignalsOnce_HTTP_2()
throws Exception {
enableProtocol(Protocol.HTTP_2);
@@ -1729,6 +1855,22 @@ public final class CallTest {
.assertRequestHeader("Accept-Encoding", "gzip");
}
+ /** https://github.com/square/okhttp/issues/1927 */
+ @Test public void gzipResponseAfterAuthenticationChallenge() throws Exception {
+ server.enqueue(new MockResponse()
+ .setResponseCode(401));
+ server.enqueue(new MockResponse()
+ .setBody(gzip("abcabcabc"))
+ .addHeader("Content-Encoding: gzip"));
+ client.setAuthenticator(new RecordingOkAuthenticator("password"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ executeSynchronously(request)
+ .assertBody("abcabcabc");
+ }
+
@Test public void asyncResponseCanBeConsumedLater() throws Exception {
server.enqueue(new MockResponse().setBody("abc"));
server.enqueue(new MockResponse().setBody("def"));
@@ -1842,6 +1984,178 @@ public final class CallTest {
.assertHeader("", "ef");
}
+ @Test public void customDns() throws Exception {
+ // Configure a DNS that returns our MockWebServer for every hostname.
+ FakeDns dns = new FakeDns();
+ dns.addresses(Dns.SYSTEM.lookup(server.url("/").host()));
+ client.setDns(dns);
+
+ server.enqueue(new MockResponse());
+ Request request = new Request.Builder()
+ .url(server.url("/").newBuilder().host("android.com").build())
+ .build();
+ executeSynchronously(request).assertCode(200);
+
+ dns.assertRequests("android.com");
+ }
+
+ /** We had a bug where failed HTTP/2 calls could break the entire connection. */
+ @Test public void failingCallsDoNotInterfereWithConnection() throws Exception {
+ enableProtocol(Protocol.HTTP_2);
+
+ server.enqueue(new MockResponse().setBody("Response 1"));
+ server.enqueue(new MockResponse().setBody("Response 2"));
+
+ RequestBody requestBody = new RequestBody() {
+ @Override public MediaType contentType() {
+ return null;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("abc");
+ sink.flush();
+
+ makeFailingCall();
+
+ sink.writeUtf8("def");
+ sink.flush();
+ }
+ };
+ Call call = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .post(requestBody)
+ .build());
+ assertEquals("Response 1", call.execute().body().string());
+ }
+
+ /** Test which headers are sent unencrypted to the HTTP proxy. */
+ @Test public void proxyConnectOmitsApplicationHeaders() throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), true);
+ server.enqueue(new MockResponse()
+ .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+ .clearHeaders());
+ server.enqueue(new MockResponse()
+ .setBody("encrypted response from the origin server"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setProxy(server.toProxyAddress());
+ RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
+ client.setHostnameVerifier(hostnameVerifier);
+
+ Request request = new Request.Builder()
+ .url("https://android.com/foo")
+ .header("Private", "Secret")
+ .header("User-Agent", "App 1.0")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("encrypted response from the origin server", response.body().string());
+
+ RecordedRequest connect = server.takeRequest();
+ assertNull(connect.getHeader("Private"));
+ assertEquals(Version.userAgent(), connect.getHeader("User-Agent"));
+ assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
+ assertEquals("android.com", connect.getHeader("Host"));
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals("Secret", get.getHeader("Private"));
+ assertEquals("App 1.0", get.getHeader("User-Agent"));
+
+ assertEquals(Arrays.asList("verify android.com"), hostnameVerifier.calls);
+ }
+
+ /** Respond to a proxy authorization challenge. */
+ @Test public void proxyAuthenticateOnConnect() throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), true);
+ server.enqueue(new MockResponse()
+ .setResponseCode(407)
+ .addHeader("Proxy-Authenticate: Basic realm=\"localhost\""));
+ server.enqueue(new MockResponse()
+ .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+ .clearHeaders());
+ server.enqueue(new MockResponse()
+ .setBody("response body"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setProxy(server.toProxyAddress());
+ client.setAuthenticator(new RecordingOkAuthenticator("password"));
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ Request request = new Request.Builder()
+ .url("https://android.com/foo")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("response body", response.body().string());
+
+ RecordedRequest connect1 = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect1.getRequestLine());
+ assertNull(connect1.getHeader("Proxy-Authorization"));
+
+ RecordedRequest connect2 = server.takeRequest();
+ assertEquals("CONNECT android.com:443 HTTP/1.1", connect2.getRequestLine());
+ assertEquals("password", connect2.getHeader("Proxy-Authorization"));
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals("GET /foo HTTP/1.1", get.getRequestLine());
+ assertNull(get.getHeader("Proxy-Authorization"));
+ }
+
+ /**
+ * Confirm that we don't send the Proxy-Authorization header from the request to the proxy server.
+ * We used to have that behavior but it is problematic because unrelated requests end up sharing
+ * credentials. Worse, that approach leaks proxy credentials to the origin server.
+ */
+ @Test public void noProactiveProxyAuthorization() throws Exception {
+ server.useHttps(sslContext.getSocketFactory(), true);
+ server.enqueue(new MockResponse()
+ .setSocketPolicy(SocketPolicy.UPGRADE_TO_SSL_AT_END)
+ .clearHeaders());
+ server.enqueue(new MockResponse()
+ .setBody("response body"));
+
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setProxy(server.toProxyAddress());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+
+ Request request = new Request.Builder()
+ .url("https://android.com/foo")
+ .header("Proxy-Authorization", "password")
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("response body", response.body().string());
+
+ RecordedRequest connect = server.takeRequest();
+ assertNull(connect.getHeader("Proxy-Authorization"));
+
+ RecordedRequest get = server.takeRequest();
+ assertEquals("password", get.getHeader("Proxy-Authorization"));
+ }
+
+ private void makeFailingCall() {
+ RequestBody requestBody = new RequestBody() {
+ @Override public MediaType contentType() {
+ return null;
+ }
+
+ @Override public long contentLength() throws IOException {
+ return 1;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ throw new IOException("write body fail!");
+ }
+ };
+ Call call = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .post(requestBody)
+ .build());
+ try {
+ call.execute();
+ fail();
+ } catch (IOException expected) {
+ assertEquals("write body fail!", expected.getMessage());
+ }
+ }
+
private RecordedResponse executeSynchronously(Request request) throws IOException {
Response response = client.newCall(request).execute();
return new RecordedResponse(request, response, null, response.body().string(), null);
@@ -1852,11 +2166,15 @@ public final class CallTest {
* -Xbootclasspath/p:/tmp/alpn-boot-8.0.0.v20140317}
*/
private void enableProtocol(Protocol protocol) {
+ enableTls();
+ client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
+ server.setProtocols(client.getProtocols());
+ }
+
+ private void enableTls() {
client.setSslSocketFactory(sslContext.getSocketFactory());
client.setHostnameVerifier(new RecordingHostnameVerifier());
- client.setProtocols(Arrays.asList(protocol, Protocol.HTTP_1_1));
server.useHttps(sslContext.getSocketFactory(), false);
- server.setProtocols(client.getProtocols());
}
private Buffer gzip(String data) throws IOException {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
index 91b5a59..7f40cd5 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/CertificatePinnerTest.java
@@ -15,12 +15,10 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.SslContextBuilder;
import java.security.GeneralSecurityException;
-import java.security.KeyPair;
-import java.security.cert.X509Certificate;
import java.util.Set;
import javax.net.ssl.SSLPeerUnverifiedException;
+import com.squareup.okhttp.internal.HeldCertificate;
import okio.ByteString;
import org.junit.Test;
@@ -32,39 +30,35 @@ import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class CertificatePinnerTest {
- static SslContextBuilder sslContextBuilder;
+ static HeldCertificate certA1;
+ static String certA1Pin;
+ static ByteString certA1PinBase64;
- static KeyPair keyPairA;
- static X509Certificate keypairACertificate1;
- static String keypairACertificate1Pin;
- static ByteString keypairACertificate1PinBase64;
+ static HeldCertificate certB1;
+ static String certB1Pin;
+ static ByteString certB1PinBase64;
- static KeyPair keyPairB;
- static X509Certificate keypairBCertificate1;
- static String keypairBCertificate1Pin;
- static ByteString keypairBCertificate1PinBase64;
-
- static KeyPair keyPairC;
- static X509Certificate keypairCCertificate1;
- static String keypairCCertificate1Pin;
+ static HeldCertificate certC1;
+ static String certC1Pin;
static {
try {
- sslContextBuilder = new SslContextBuilder("example.com");
-
- keyPairA = sslContextBuilder.generateKeyPair();
- keypairACertificate1 = sslContextBuilder.selfSignedCertificate(keyPairA, "1");
- keypairACertificate1Pin = CertificatePinner.pin(keypairACertificate1);
- keypairACertificate1PinBase64 = pinToBase64(keypairACertificate1Pin);
-
- keyPairB = sslContextBuilder.generateKeyPair();
- keypairBCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairB, "1");
- keypairBCertificate1Pin = CertificatePinner.pin(keypairBCertificate1);
- keypairBCertificate1PinBase64 = pinToBase64(keypairBCertificate1Pin);
-
- keyPairC = sslContextBuilder.generateKeyPair();
- keypairCCertificate1 = sslContextBuilder.selfSignedCertificate(keyPairC, "1");
- keypairCCertificate1Pin = CertificatePinner.pin(keypairCCertificate1);
+ certA1 = new HeldCertificate.Builder()
+ .serialNumber("100")
+ .build();
+ certA1Pin = CertificatePinner.pin(certA1.certificate);
+ certA1PinBase64 = pinToBase64(certA1Pin);
+
+ certB1 = new HeldCertificate.Builder()
+ .serialNumber("200")
+ .build();
+ certB1Pin = CertificatePinner.pin(certB1.certificate);
+ certB1PinBase64 = pinToBase64(certB1Pin);
+
+ certC1 = new HeldCertificate.Builder()
+ .serialNumber("300")
+ .build();
+ certC1Pin = CertificatePinner.pin(certC1.certificate);
} catch (GeneralSecurityException e) {
throw new AssertionError(e);
}
@@ -94,40 +88,46 @@ public final class CertificatePinnerTest {
/** Multiple certificates generated from the same keypair have the same pin. */
@Test public void sameKeypairSamePin() throws Exception {
- X509Certificate keypairACertificate2 = sslContextBuilder.selfSignedCertificate(keyPairA, "2");
- String keypairACertificate2Pin = CertificatePinner.pin(keypairACertificate2);
+ HeldCertificate heldCertificateA2 = new HeldCertificate.Builder()
+ .keyPair(certA1.keyPair)
+ .serialNumber("101")
+ .build();
+ String keypairACertificate2Pin = CertificatePinner.pin(heldCertificateA2.certificate);
- X509Certificate keypairBCertificate2 = sslContextBuilder.selfSignedCertificate(keyPairB, "2");
- String keypairBCertificate2Pin = CertificatePinner.pin(keypairBCertificate2);
+ HeldCertificate heldCertificateB2 = new HeldCertificate.Builder()
+ .keyPair(certB1.keyPair)
+ .serialNumber("201")
+ .build();
+ String keypairBCertificate2Pin = CertificatePinner.pin(heldCertificateB2.certificate);
- assertTrue(keypairACertificate1Pin.equals(keypairACertificate2Pin));
- assertTrue(keypairBCertificate1Pin.equals(keypairBCertificate2Pin));
- assertFalse(keypairACertificate1Pin.equals(keypairBCertificate1Pin));
+ assertTrue(certA1Pin.equals(keypairACertificate2Pin));
+ assertTrue(certB1Pin.equals(keypairBCertificate2Pin));
+ assertFalse(certA1Pin.equals(certB1Pin));
}
@Test public void successfulCheck() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin)
+ .add("example.com", certA1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
}
@Test public void successfulMatchAcceptsAnyMatchingCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairBCertificate1Pin)
+ .add("example.com", certB1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1, keypairBCertificate1);
+ certificatePinner.check("example.com", certA1.certificate, certB1.certificate);
}
@Test public void unsuccessfulCheck() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin)
+ .add("example.com", certA1Pin)
.build();
try {
- certificatePinner.check("example.com", keypairBCertificate1);
+ certificatePinner.check("example.com", certB1.certificate);
fail();
} catch (SSLPeerUnverifiedException expected) {
}
@@ -135,51 +135,51 @@ public final class CertificatePinnerTest {
@Test public void multipleCertificatesForOneHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+ .add("example.com", certA1Pin, certB1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1);
- certificatePinner.check("example.com", keypairBCertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
+ certificatePinner.check("example.com", certB1.certificate);
}
@Test public void multipleHostnamesForOneCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("example.com", keypairACertificate1Pin)
- .add("www.example.com", keypairACertificate1Pin)
+ .add("example.com", certA1Pin)
+ .add("www.example.com", certA1Pin)
.build();
- certificatePinner.check("example.com", keypairACertificate1);
- certificatePinner.check("www.example.com", keypairACertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
+ certificatePinner.check("www.example.com", certA1.certificate);
}
@Test public void absentHostnameMatches() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder().build();
- certificatePinner.check("example.com", keypairACertificate1);
+ certificatePinner.check("example.com", certA1.certificate);
}
@Test public void successfulCheckForWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
+ .add("*.example.com", certA1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate);
}
@Test public void successfulMatchAcceptsAnyMatchingCertificateForWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairBCertificate1Pin)
+ .add("*.example.com", certB1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1, keypairBCertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate, certB1.certificate);
}
@Test public void unsuccessfulCheckForWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
+ .add("*.example.com", certA1Pin)
.build();
try {
- certificatePinner.check("a.example.com", keypairBCertificate1);
+ certificatePinner.check("a.example.com", certB1.certificate);
fail();
} catch (SSLPeerUnverifiedException expected) {
}
@@ -187,31 +187,31 @@ public final class CertificatePinnerTest {
@Test public void multipleCertificatesForOneWildcardHostname() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin, keypairBCertificate1Pin)
+ .add("*.example.com", certA1Pin, certB1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1);
- certificatePinner.check("a.example.com", keypairBCertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate);
+ certificatePinner.check("a.example.com", certB1.certificate);
}
@Test public void successfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
- .add("a.example.com", keypairBCertificate1Pin)
+ .add("*.example.com", certA1Pin)
+ .add("a.example.com", certB1Pin)
.build();
- certificatePinner.check("a.example.com", keypairACertificate1);
- certificatePinner.check("a.example.com", keypairBCertificate1);
+ certificatePinner.check("a.example.com", certA1.certificate);
+ certificatePinner.check("a.example.com", certB1.certificate);
}
@Test public void unsuccessfulCheckForOneHostnameWithWildcardAndDirectCertificate() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
- .add("a.example.com", keypairBCertificate1Pin)
+ .add("*.example.com", certA1Pin)
+ .add("a.example.com", certB1Pin)
.build();
try {
- certificatePinner.check("a.example.com", keypairCCertificate1);
+ certificatePinner.check("a.example.com", certC1.certificate);
fail();
} catch (SSLPeerUnverifiedException expected) {
}
@@ -219,32 +219,32 @@ public final class CertificatePinnerTest {
@Test public void successfulFindMatchingPins() {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("first.com", keypairACertificate1Pin, keypairBCertificate1Pin)
- .add("second.com", keypairCCertificate1Pin)
+ .add("first.com", certA1Pin, certB1Pin)
+ .add("second.com", certC1Pin)
.build();
- Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
- Set<ByteString> matchedPins = certificatePinner.findMatchingPins("first.com");
+ Set<ByteString> expectedPins = setOf(certA1PinBase64, certB1PinBase64);
+ Set<ByteString> matchedPins = certificatePinner.findMatchingPins("first.com");
assertEquals(expectedPins, matchedPins);
}
@Test public void successfulFindMatchingPinsForWildcardAndDirectCertificates() {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
- .add("a.example.com", keypairBCertificate1Pin)
- .add("b.example.com", keypairCCertificate1Pin)
+ .add("*.example.com", certA1Pin)
+ .add("a.example.com", certB1Pin)
+ .add("b.example.com", certC1Pin)
.build();
- Set<ByteString> expectedPins = setOf(keypairACertificate1PinBase64, keypairBCertificate1PinBase64);
- Set<ByteString> matchedPins = certificatePinner.findMatchingPins("a.example.com");
+ Set<ByteString> expectedPins = setOf(certA1PinBase64, certB1PinBase64);
+ Set<ByteString> matchedPins = certificatePinner.findMatchingPins("a.example.com");
assertEquals(expectedPins, matchedPins);
}
@Test public void wildcardHostnameShouldNotMatchThroughDot() throws Exception {
CertificatePinner certificatePinner = new CertificatePinner.Builder()
- .add("*.example.com", keypairACertificate1Pin)
+ .add("*.example.com", certA1Pin)
.build();
assertNull(certificatePinner.findMatchingPins("example.com"));
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
index d528c7a..e845eec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 Square, Inc.
+ * Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,570 +15,197 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.SslContextBuilder;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.http.AuthenticatorAdapter;
-import com.squareup.okhttp.internal.http.RecordingProxySelector;
-import com.squareup.okhttp.mockwebserver.MockWebServer;
-import com.squareup.okhttp.testing.RecordingHostnameVerifier;
-import java.io.IOException;
-import java.net.InetAddress;
+import com.squareup.okhttp.internal.RecordingOkAuthenticator;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
import java.net.InetSocketAddress;
import java.net.Proxy;
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Executor;
+import java.net.ProxySelector;
+import java.net.Socket;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
import javax.net.SocketFactory;
-import javax.net.ssl.SSLContext;
-import org.junit.After;
-import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
public final class ConnectionPoolTest {
- static {
- Internal.initializeInstanceForTests();
- }
-
- private static final List<ConnectionSpec> CONNECTION_SPECS = Util.immutableList(
- ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
-
- private static final int KEEP_ALIVE_DURATION_MS = 5000;
-
- private SSLContext sslContext = SslContextBuilder.localhost();
- private MockWebServer spdyServer;
- private InetSocketAddress spdySocketAddress;
- private Address spdyAddress;
-
- private MockWebServer httpServer;
- private Address httpAddress;
- private InetSocketAddress httpSocketAddress;
-
- private ConnectionPool pool;
- private FakeExecutor cleanupExecutor;
- private Connection httpA;
- private Connection httpB;
- private Connection httpC;
- private Connection httpD;
- private Connection httpE;
- private Connection spdyA;
-
- private Object owner;
-
- @Before public void setUp() throws Exception {
- setUp(2);
- }
-
- private void setUp(int poolSize) throws Exception {
- SocketFactory socketFactory = SocketFactory.getDefault();
- RecordingProxySelector proxySelector = new RecordingProxySelector();
-
- spdyServer = new MockWebServer();
- httpServer = new MockWebServer();
- spdyServer.useHttps(sslContext.getSocketFactory(), false);
-
- httpServer.start();
- httpAddress = new Address(httpServer.getHostName(), httpServer.getPort(), socketFactory, null,
- null, null, AuthenticatorAdapter.INSTANCE, null,
- Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1), CONNECTION_SPECS, proxySelector);
- httpSocketAddress = new InetSocketAddress(InetAddress.getByName(httpServer.getHostName()),
- httpServer.getPort());
-
- spdyServer.start();
- spdyAddress = new Address(spdyServer.getHostName(), spdyServer.getPort(), socketFactory,
- sslContext.getSocketFactory(), new RecordingHostnameVerifier(), CertificatePinner.DEFAULT,
- AuthenticatorAdapter.INSTANCE, null, Util.immutableList(Protocol.SPDY_3, Protocol.HTTP_1_1),
- CONNECTION_SPECS, proxySelector);
- spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
- spdyServer.getPort());
-
- Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress);
- Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress);
- pool = new ConnectionPool(poolSize, KEEP_ALIVE_DURATION_MS);
- // Disable the automatic execution of the cleanup.
- cleanupExecutor = new FakeExecutor();
- pool.replaceCleanupExecutorForTests(cleanupExecutor);
- httpA = new Connection(pool, httpRoute);
- httpA.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpB = new Connection(pool, httpRoute);
- httpB.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpC = new Connection(pool, httpRoute);
- httpC.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpD = new Connection(pool, httpRoute);
- httpD.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- httpE = new Connection(pool, httpRoute);
- httpE.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- spdyA = new Connection(pool, spdyRoute);
- spdyA.connect(20000, 20000, 2000, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
-
- owner = new Object();
- httpA.setOwner(owner);
- httpB.setOwner(owner);
- httpC.setOwner(owner);
- httpD.setOwner(owner);
- httpE.setOwner(owner);
- }
-
- @After public void tearDown() throws Exception {
- httpServer.shutdown();
- spdyServer.shutdown();
-
- Util.closeQuietly(httpA.getSocket());
- Util.closeQuietly(httpB.getSocket());
- Util.closeQuietly(httpC.getSocket());
- Util.closeQuietly(httpD.getSocket());
- Util.closeQuietly(httpE.getSocket());
- Util.closeQuietly(spdyA.getSocket());
- }
+ private final Runnable emptyRunnable = new Runnable() {
+ @Override public void run() {
+ }
+ };
- private void resetWithPoolSize(int poolSize) throws Exception {
- tearDown();
- setUp(poolSize);
- }
+ private final Address addressA = newAddress("a");
+ private final Route routeA1 = newRoute(addressA);
+ private final Address addressB = newAddress("b");
+ private final Route routeB1 = newRoute(addressB);
+ private final Address addressC = newAddress("c");
+ private final Route routeC1 = newRoute(addressC);
- @Test public void poolSingleHttpConnection() throws Exception {
- resetWithPoolSize(1);
- Connection connection = pool.get(httpAddress);
- assertNull(connection);
+ @Test public void connectionsEvictedWhenIdleLongEnough() throws Exception {
+ ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress));
- connection.connect(200, 200, 200, null, CONNECTION_SPECS, false /* connectionRetryEnabled */);
- connection.setOwner(owner);
- assertEquals(0, pool.getConnectionCount());
+ RealConnection c1 = newConnection(pool, routeA1, 50L);
- pool.recycle(connection);
- assertNull(connection.getOwner());
+ // Running at time 50, the pool returns that nothing can be evicted until time 150.
+ assertEquals(100L, pool.cleanup(50L));
assertEquals(1, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- Connection recycledConnection = pool.get(httpAddress);
- assertNull(connection.getOwner());
- assertEquals(connection, recycledConnection);
- assertTrue(recycledConnection.isAlive());
-
- recycledConnection = pool.get(httpAddress);
- assertNull(recycledConnection);
- }
-
- @Test public void getDoesNotScheduleCleanup() {
- Connection connection = pool.get(httpAddress);
- assertNull(connection);
- cleanupExecutor.assertExecutionScheduled(false);
- }
-
- @Test public void recycleSchedulesCleanup() {
- cleanupExecutor.assertExecutionScheduled(false);
- pool.recycle(httpA);
- cleanupExecutor.assertExecutionScheduled(true);
- }
-
- @Test public void shareSchedulesCleanup() {
- cleanupExecutor.assertExecutionScheduled(false);
- pool.share(spdyA);
- cleanupExecutor.assertExecutionScheduled(true);
- }
-
- @Test public void poolPrefersMostRecentlyRecycled() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
- pool.recycle(httpC);
- assertPooled(pool, httpC, httpB, httpA);
-
- pool.performCleanup();
- assertPooled(pool, httpC, httpB);
- }
-
- @Test public void getSpdyConnection() throws Exception {
- pool.share(spdyA);
- assertSame(spdyA, pool.get(spdyAddress));
- assertPooled(pool, spdyA);
- }
-
- @Test public void getHttpConnection() throws Exception {
- pool.recycle(httpA);
- assertSame(httpA, pool.get(httpAddress));
- assertPooled(pool);
- }
-
- @Test public void expiredConnectionNotReturned() throws Exception {
- pool.recycle(httpA);
-
- // Allow enough time to pass so that the connection is now expired.
- Thread.sleep(KEEP_ALIVE_DURATION_MS * 2);
-
- // The connection is held, but will not be returned.
- assertNull(pool.get(httpAddress));
- assertPooled(pool, httpA);
-
- // The connection must be cleaned up.
- pool.performCleanup();
- assertPooled(pool);
- }
-
- @Test public void maxIdleConnectionLimitIsEnforced() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
- pool.recycle(httpC);
- pool.recycle(httpD);
- assertPooled(pool, httpD, httpC, httpB, httpA);
-
- pool.performCleanup();
- assertPooled(pool, httpD, httpC);
- }
-
- @Test public void expiredConnectionsAreEvicted() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
+ assertFalse(c1.socket.isClosed());
- // Allow enough time to pass so that the connections are now expired.
- Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
- assertPooled(pool, httpB, httpA);
-
- // The connections must be cleaned up.
- pool.performCleanup();
- assertPooled(pool);
- }
-
- @Test public void nonAliveConnectionNotReturned() throws Exception {
- pool.recycle(httpA);
-
- // Close the connection. It is an ex-connection. It has ceased to be.
- httpA.getSocket().close();
- assertPooled(pool, httpA);
- assertNull(pool.get(httpAddress));
-
- // The connection must be cleaned up.
- pool.performCleanup();
- assertPooled(pool);
- }
-
- @Test public void differentAddressConnectionNotReturned() throws Exception {
- pool.recycle(httpA);
- assertNull(pool.get(spdyAddress));
- assertPooled(pool, httpA);
- }
-
- @Test public void gettingSpdyConnectionPromotesItToFrontOfQueue() throws Exception {
- pool.share(spdyA);
- pool.recycle(httpA);
- assertPooled(pool, httpA, spdyA);
- assertSame(spdyA, pool.get(spdyAddress));
- assertPooled(pool, spdyA, httpA);
- }
-
- @Test public void gettingConnectionReturnsOldestFirst() throws Exception {
- pool.recycle(httpA);
- pool.recycle(httpB);
- assertSame(httpA, pool.get(httpAddress));
- }
-
- @Test public void recyclingNonAliveConnectionClosesThatConnection() throws Exception {
- httpA.getSocket().shutdownInput();
- pool.recycle(httpA); // Should close httpA.
- assertTrue(httpA.getSocket().isClosed());
+ // Running at time 60, the pool returns that nothing can be evicted until time 150.
+ assertEquals(90L, pool.cleanup(60L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- // The pool should remain empty, and there is no need to schedule a cleanup.
- assertPooled(pool);
- cleanupExecutor.assertExecutionScheduled(false);
- }
+ // Running at time 149, the pool returns that nothing can be evicted until time 150.
+ assertEquals(1L, pool.cleanup(149L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- @Test public void shareHttpConnectionFails() throws Exception {
- try {
- pool.share(httpA);
- fail();
- } catch (IllegalArgumentException expected) {
- }
- // The pool should remain empty, and there is no need to schedule a cleanup.
- assertPooled(pool);
- cleanupExecutor.assertExecutionScheduled(false);
- }
+ // Running at time 150, the pool evicts.
+ assertEquals(0, pool.cleanup(150L));
+ assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
- @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);
+ // Running again, the pool reports that no further runs are necessary.
+ assertEquals(-1, pool.cleanup(150L));
+ assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
}
- @Test public void validateIdleSpdyConnectionTimeout() throws Exception {
- pool.share(spdyA);
- 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.
+ @Test public void inUseConnectionsNotEvicted() throws Exception {
+ ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
- pool.performCleanup();
- assertPooled(pool); // Connection should have been removed.
- }
+ RealConnection c1 = newConnection(pool, routeA1, 50L);
+ StreamAllocation streamAllocation = new StreamAllocation(pool, addressA);
+ streamAllocation.acquire(c1);
- @Test public void validateIdleHttpConnectionTimeout() throws Exception {
- pool.recycle(httpA);
- assertPooled(pool, httpA); // Connection should be in the pool.
- cleanupExecutor.assertExecutionScheduled(true);
+ // Running at time 50, the pool returns that nothing can be evicted until time 150.
+ assertEquals(100L, pool.cleanup(50L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.7));
- pool.performCleanup();
- assertPooled(pool, httpA); // Connection should still be in the pool.
+ // Running at time 60, the pool returns that nothing can be evicted until time 160.
+ assertEquals(100L, pool.cleanup(60L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
- Thread.sleep((long) (KEEP_ALIVE_DURATION_MS * 0.4));
- pool.performCleanup();
- assertPooled(pool); // Connection should have been removed.
+ // Running at time 160, the pool returns that nothing can be evicted until time 260.
+ assertEquals(100L, pool.cleanup(160L));
+ assertEquals(1, pool.getConnectionCount());
+ assertFalse(c1.socket.isClosed());
}
- @Test public void maxConnections() throws IOException, InterruptedException {
- // Pool should be empty.
- assertEquals(0, pool.getConnectionCount());
+ @Test public void cleanupPrioritizesEarliestEviction() throws Exception {
+ ConnectionPool pool = new ConnectionPool(Integer.MAX_VALUE, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- // http A should be added to the pool.
- pool.recycle(httpA);
- assertEquals(1, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
+ RealConnection c1 = newConnection(pool, routeA1, 75L);
+ RealConnection c2 = newConnection(pool, routeB1, 50L);
- // http B should be added to the pool.
- pool.recycle(httpB);
+ // Running at time 75, the pool returns that nothing can be evicted until time 150.
+ assertEquals(75L, pool.cleanup(75L));
assertEquals(2, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- // http C should be added
- pool.recycle(httpC);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(3, pool.getHttpConnectionCount());
- assertEquals(0, pool.getSpdyConnectionCount());
- pool.performCleanup();
-
- // http A should be removed by cleanup.
+ // Running at time 149, the pool returns that nothing can be evicted until time 150.
+ assertEquals(1L, pool.cleanup(149L));
assertEquals(2, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
-
- // spdy A should be added
- pool.share(spdyA);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
- pool.performCleanup();
-
- // http B should be removed by cleanup.
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // http C should be returned.
- Connection recycledHttpConnection = pool.get(httpAddress);
- recycledHttpConnection.setOwner(owner);
- assertNotNull(recycledHttpConnection);
- assertTrue(recycledHttpConnection.isAlive());
+ // Running at time 150, the pool evicts c2.
+ assertEquals(0L, pool.cleanup(150L));
assertEquals(1, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
+ assertFalse(c1.socket.isClosed());
+ assertTrue(c2.socket.isClosed());
- // spdy A will be returned but also kept in the pool.
- Connection sharedSpdyConnection = pool.get(spdyAddress);
- assertNotNull(sharedSpdyConnection);
- assertEquals(spdyA, sharedSpdyConnection);
+ // Running at time 150, the pool returns that nothing can be evicted until time 175.
+ assertEquals(25L, pool.cleanup(150L));
assertEquals(1, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
- // http C should be added to the pool
- pool.recycle(httpC);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
+ // Running at time 175, the pool evicts c1.
+ assertEquals(0L, pool.cleanup(175L));
+ assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.socket.isClosed());
+ assertTrue(c2.socket.isClosed());
+ }
- // An http connection should be removed from the pool.
- recycledHttpConnection = pool.get(httpAddress);
- assertNotNull(recycledHttpConnection);
- assertTrue(recycledHttpConnection.isAlive());
- assertEquals(1, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
+ @Test public void oldestConnectionsEvictedIfIdleLimitExceeded() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- // spdy A will be returned but also kept in the pool.
- sharedSpdyConnection = pool.get(spdyAddress);
- assertEquals(spdyA, sharedSpdyConnection);
- assertNotNull(sharedSpdyConnection);
- assertEquals(1, pool.getConnectionCount());
- assertEquals(0, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
+ RealConnection c1 = newConnection(pool, routeA1, 50L);
+ RealConnection c2 = newConnection(pool, routeB1, 75L);
- // http D should be added to the pool.
- pool.recycle(httpD);
+ // With 2 connections, there's no need to evict until the connections time out.
+ assertEquals(50L, pool.cleanup(100L));
assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
+ assertFalse(c1.socket.isClosed());
+ assertFalse(c2.socket.isClosed());
- // http E should be added to the pool.
- pool.recycle(httpE);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+ // Add a third connection
+ RealConnection c3 = newConnection(pool, routeC1, 75L);
- pool.performCleanup();
-
- // spdy A should be removed from the pool by cleanup.
+ // The third connection bounces the first.
+ assertEquals(0L, pool.cleanup(100L));
assertEquals(2, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(0, pool.getMultiplexedConnectionCount());
+ assertTrue(c1.socket.isClosed());
+ assertFalse(c2.socket.isClosed());
+ assertFalse(c3.socket.isClosed());
}
- @Test public void connectionCleanup() throws Exception {
- ConnectionPool pool = new ConnectionPool(10, KEEP_ALIVE_DURATION_MS);
-
- // Add 3 connections to the pool.
- pool.recycle(httpA);
- pool.recycle(httpB);
- pool.share(spdyA);
-
- // Give the cleanup callable time to run and settle down.
- Thread.sleep(100);
-
- // Kill http A.
- Util.closeQuietly(httpA.getSocket());
+ @Test public void leakedAllocation() throws Exception {
+ ConnectionPool pool = new ConnectionPool(2, 100L, TimeUnit.NANOSECONDS);
+ pool.setCleanupRunnableForTest(emptyRunnable);
- assertEquals(3, pool.getConnectionCount());
- assertEquals(2, pool.getHttpConnectionCount());
- assertEquals(1, pool.getSpdyConnectionCount());
+ RealConnection c1 = newConnection(pool, routeA1, 0L);
+ allocateAndLeakAllocation(pool, c1);
- // Http A should be removed.
- pool.performCleanup();
- assertPooled(pool, spdyA, httpB);
- assertEquals(2, pool.getConnectionCount());
- assertEquals(1, pool.getHttpConnectionCount());
- assertEquals(1, pool.getMultiplexedConnectionCount());
-
- // Now let enough time pass for the connections to expire.
- Thread.sleep(2 * KEEP_ALIVE_DURATION_MS);
-
- // All remaining connections should be removed.
- pool.performCleanup();
- assertEquals(0, pool.getConnectionCount());
- }
-
- @Test public void maxIdleConnectionsLimitEnforced() throws Exception {
- ConnectionPool pool = new ConnectionPool(2, KEEP_ALIVE_DURATION_MS);
-
- // Hit the max idle connections limit of 2.
- pool.recycle(httpA);
- pool.recycle(httpB);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpB, httpA);
-
- // Adding httpC bumps httpA.
- pool.recycle(httpC);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpC, httpB);
-
- // Adding httpD bumps httpB.
- pool.recycle(httpD);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpD, httpC);
-
- // Adding httpE bumps httpC.
- pool.recycle(httpE);
- Thread.sleep(100); // Give the cleanup callable time to run.
- assertPooled(pool, httpE, httpD);
- }
+ awaitGarbageCollection();
+ assertEquals(0L, pool.cleanup(100L));
+ assertEquals(Collections.emptyList(), c1.allocations);
- @Test public void evictAllConnections() throws Exception {
- resetWithPoolSize(10);
- pool.recycle(httpA);
- Util.closeQuietly(httpA.getSocket()); // Include a closed connection in the pool.
- pool.recycle(httpB);
- pool.share(spdyA);
- int connectionCount = pool.getConnectionCount();
- assertTrue(connectionCount == 2 || connectionCount == 3);
-
- pool.evictAll();
- assertEquals(0, pool.getConnectionCount());
+ assertTrue(c1.noNewStreams); // Can't allocate once a leak has been detected.
}
- @Test public void closeIfOwnedBy() throws Exception {
- httpA.closeIfOwnedBy(owner);
- assertFalse(httpA.isAlive());
- assertFalse(httpA.clearOwner());
+ /** Use a helper method so there's no hidden reference remaining on the stack. */
+ private void allocateAndLeakAllocation(ConnectionPool pool, RealConnection connection) {
+ StreamAllocation leak = new StreamAllocation(pool, connection.getRoute().getAddress());
+ leak.acquire(connection);
}
- @Test public void closeIfOwnedByDoesNothingIfNotOwner() throws Exception {
- httpA.closeIfOwnedBy(new Object());
- assertTrue(httpA.isAlive());
- assertTrue(httpA.clearOwner());
+ /**
+ * See FinalizationTester for discussion on how to best trigger GC in tests.
+ * https://android.googlesource.com/platform/libcore/+/master/support/src/test/java/libcore/
+ * java/lang/ref/FinalizationTester.java
+ */
+ private void awaitGarbageCollection() throws InterruptedException {
+ Runtime.getRuntime().gc();
+ Thread.sleep(100);
+ System.runFinalization();
}
- @Test public void closeIfOwnedByFailsForSpdyConnections() throws Exception {
- try {
- spdyA.closeIfOwnedBy(owner);
- fail();
- } catch (IllegalStateException expected) {
+ private RealConnection newConnection(ConnectionPool pool, Route route, long idleAtNanos) {
+ RealConnection connection = new RealConnection(route);
+ connection.idleAtNanos = idleAtNanos;
+ connection.socket = new Socket();
+ synchronized (pool) {
+ pool.put(connection);
}
+ return connection;
}
- @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());
+ private Address newAddress(String name) {
+ return new Address(name, 1, Dns.SYSTEM, SocketFactory.getDefault(), null, null, null,
+ new RecordingOkAuthenticator("password"), null, Collections.<Protocol>emptyList(),
+ Collections.<ConnectionSpec>emptyList(),
+ ProxySelector.getDefault());
}
- /**
- * 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();
- }
+ private Route newRoute(Address address) {
+ return new Route(address, Proxy.NO_PROXY,
+ InetSocketAddress.createUnresolved(address.url().host(), address.url().port()));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java
new file mode 100644
index 0000000..f445dac
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionReuseTest.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+
+import static org.junit.Assert.assertEquals;
+
+public final class ConnectionReuseTest {
+ @Rule public final TestRule timeout = new Timeout(30_000);
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ private SSLContext sslContext = SslContextBuilder.localhost();
+ private OkHttpClient client = new OkHttpClient();
+
+ @Test public void connectionsAreReused() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionReused(request, request);
+ }
+
+ @Test public void connectionsAreReusedWithHttp2() throws Exception {
+ enableHttp2();
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionReused(request, request);
+ }
+
+ @Test public void connectionsAreNotReusedWithRequestConnectionClose() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request requestA = new Request.Builder()
+ .url(server.url("/"))
+ .header("Connection", "close")
+ .build();
+ Request requestB = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(requestA, requestB);
+ }
+
+ @Test public void connectionsAreNotReusedWithResponseConnectionClose() throws Exception {
+ server.enqueue(new MockResponse()
+ .addHeader("Connection", "close")
+ .setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request requestA = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Request requestB = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(requestA, requestB);
+ }
+
+ @Test public void connectionsAreNotReusedWithUnknownLengthResponseBody() throws Exception {
+ server.enqueue(new MockResponse()
+ .setBody("a")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END)
+ .clearHeaders());
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(request, request);
+ }
+
+ @Test public void connectionsAreNotReusedIfPoolIsSizeZero() throws Exception {
+ client.setConnectionPool(new ConnectionPool(0, 5000));
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ assertConnectionNotReused(request, request);
+ }
+
+ @Test public void connectionsReusedWithRedirectEvenIfPoolIsSizeZero() throws Exception {
+ client.setConnectionPool(new ConnectionPool(0, 5000));
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /b")
+ .setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("b", response.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void connectionsNotReusedWithRedirectIfDiscardingResponseIsSlow() throws Exception {
+ client.setConnectionPool(new ConnectionPool(0, 5000));
+ server.enqueue(new MockResponse()
+ .setResponseCode(301)
+ .addHeader("Location: /b")
+ .setBodyDelay(1, TimeUnit.SECONDS)
+ .setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response response = client.newCall(request).execute();
+ assertEquals("b", response.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void silentRetryWhenIdempotentRequestFailsOnReusedConnection() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.DISCONNECT_AFTER_REQUEST));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+
+ Response responseA = client.newCall(request).execute();
+ assertEquals("a", responseA.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+
+ Response responseB = client.newCall(request).execute();
+ assertEquals("b", responseB.body().string());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void staleConnectionNotReusedForNonIdempotentRequest() throws Exception {
+ server.enqueue(new MockResponse().setBody("a")
+ .setSocketPolicy(SocketPolicy.SHUTDOWN_OUTPUT_AT_END));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request requestA = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response responseA = client.newCall(requestA).execute();
+ assertEquals("a", responseA.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+
+ Request requestB = new Request.Builder()
+ .url(server.url("/"))
+ .post(RequestBody.create(MediaType.parse("text/plain"), "b"))
+ .build();
+ Response responseB = client.newCall(requestB).execute();
+ assertEquals("b", responseB.body().string());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void http2ConnectionsAreSharedBeforeResponseIsConsumed() throws Exception {
+ enableHttp2();
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Response response1 = client.newCall(request).execute();
+ Response response2 = client.newCall(request).execute();
+ response1.body().string(); // Discard the response body.
+ response2.body().string(); // Discard the response body.
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(1, server.takeRequest().getSequenceNumber());
+ }
+
+ @Test public void connectionsAreEvicted() throws Exception {
+ server.enqueue(new MockResponse().setBody("a"));
+ server.enqueue(new MockResponse().setBody("b"));
+
+ client.setConnectionPool(new ConnectionPool(5, 250, TimeUnit.MILLISECONDS));
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+
+ Response response1 = client.newCall(request).execute();
+ assertEquals("a", response1.body().string());
+
+ // Give the thread pool a chance to evict.
+ Thread.sleep(500);
+
+ Response response2 = client.newCall(request).execute();
+ assertEquals("b", response2.body().string());
+
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+
+ private void enableHttp2() {
+ client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ client.setProtocols(Arrays.asList(Protocol.HTTP_2, Protocol.HTTP_1_1));
+ server.useHttps(sslContext.getSocketFactory(), false);
+ server.setProtocols(client.getProtocols());
+ }
+
+ private void assertConnectionReused(Request... requests) throws Exception {
+ for (int i = 0; i < requests.length; i++) {
+ Response response = client.newCall(requests[i]).execute();
+ response.body().string(); // Discard the response body.
+ assertEquals(i, server.takeRequest().getSequenceNumber());
+ }
+ }
+
+ private void assertConnectionNotReused(Request... requests) throws Exception {
+ for (Request request : requests) {
+ Response response = client.newCall(request).execute();
+ response.body().string(); // Discard the response body.
+ assertEquals(0, server.takeRequest().getSequenceNumber());
+ }
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
index 7833cca..3b390e8 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionSpecTest.java
@@ -15,29 +15,49 @@
*/
package com.squareup.okhttp;
-import org.junit.Test;
-
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
+import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
public final class ConnectionSpecTest {
+ @Test public void noTlsVersions() throws Exception {
+ try {
+ new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(new TlsVersion[0])
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {
+ assertEquals("At least one TLS version is required", expected.getMessage());
+ }
+ }
- @Test
- public void cleartextBuilder() throws Exception {
+ @Test public void noCipherSuites() throws Exception {
+ try {
+ new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .cipherSuites(new CipherSuite[0])
+ .build();
+ fail();
+ } catch (IllegalArgumentException expected) {
+ assertEquals("At least one cipher suite is required", expected.getMessage());
+ }
+ }
+
+ @Test public void cleartextBuilder() throws Exception {
ConnectionSpec cleartextSpec = new ConnectionSpec.Builder(false).build();
assertFalse(cleartextSpec.isTls());
}
- @Test
- public void tlsBuilder_explicitCiphers() throws Exception {
+ @Test public void tlsBuilder_explicitCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -48,8 +68,7 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.supportsTlsExtensions());
}
- @Test
- public void tlsBuilder_defaultCiphers() throws Exception {
+ @Test public void tlsBuilder_defaultCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(true)
@@ -59,8 +78,7 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.supportsTlsExtensions());
}
- @Test
- public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
+ @Test public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(false)
@@ -79,17 +97,16 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, false /* isFallback */);
- assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+ assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet =
- createSet(
+ set(
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
assertEquals(expectedCipherSet, expectedCipherSet);
}
- @Test
- public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
+ @Test public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(false)
@@ -108,10 +125,10 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, true /* isFallback */);
- assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+ assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet =
- createSet(
+ set(
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
@@ -120,8 +137,7 @@ public final class ConnectionSpecTest {
assertEquals(expectedCipherSet, expectedCipherSet);
}
- @Test
- public void tls_explicitCiphers() throws Exception {
+ @Test public void tls_explicitCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -141,17 +157,16 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, true /* isFallback */);
- assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
+ assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
- Set<String> expectedCipherSet = createSet(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
+ Set<String> expectedCipherSet = set(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
expectedCipherSet.add("TLS_FALLBACK_SCSV");
}
assertEquals(expectedCipherSet, expectedCipherSet);
}
- @Test
- public void tls_stringCiphersAndVersions() throws Exception {
+ @Test public void tls_stringCiphersAndVersions() throws Exception {
// Supporting arbitrary input strings allows users to enable suites and versions that are not
// yet known to the library, but are supported by the platform.
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
@@ -160,7 +175,7 @@ public final class ConnectionSpecTest {
.build();
}
- public void tls_missingRequiredCipher() throws Exception {
+ @Test public void tls_missingRequiredCipher() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -185,8 +200,43 @@ public final class ConnectionSpecTest {
assertFalse(tlsSpec.isCompatible(socket));
}
- @Test
- public void tls_missingTlsVersion() throws Exception {
+ @Test public void allEnabledCipherSuites() throws Exception {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledCipherSuites()
+ .build();
+ assertNull(tlsSpec.cipherSuites());
+
+ SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ sslSocket.setEnabledCipherSuites(new String[] {
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
+ });
+
+ tlsSpec.apply(sslSocket, false);
+ assertEquals(Arrays.asList(
+ CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
+ CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName),
+ Arrays.asList(sslSocket.getEnabledCipherSuites()));
+ }
+
+ @Test public void allEnabledTlsVersions() throws Exception {
+ ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledTlsVersions()
+ .build();
+ assertNull(tlsSpec.tlsVersions());
+
+ SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
+ sslSocket.setEnabledProtocols(new String[] {
+ TlsVersion.SSL_3_0.javaName(),
+ TlsVersion.TLS_1_1.javaName()
+ });
+
+ tlsSpec.apply(sslSocket, false);
+ assertEquals(Arrays.asList(TlsVersion.SSL_3_0.javaName(), TlsVersion.TLS_1_1.javaName()),
+ Arrays.asList(sslSocket.getEnabledProtocols()));
+ }
+
+ @Test public void tls_missingTlsVersion() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -206,7 +256,48 @@ public final class ConnectionSpecTest {
assertFalse(tlsSpec.isCompatible(socket));
}
- private static Set<String> createSet(String... values) {
- return new LinkedHashSet<String>(Arrays.asList(values));
+ @Test public void equalsAndHashCode() throws Exception {
+ ConnectionSpec allCipherSuites = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledCipherSuites()
+ .build();
+ ConnectionSpec allTlsVersions = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledTlsVersions()
+ .build();
+
+ Set<Object> set = new CopyOnWriteArraySet<>();
+ assertTrue(set.add(ConnectionSpec.MODERN_TLS));
+ assertTrue(set.add(ConnectionSpec.COMPATIBLE_TLS));
+ assertTrue(set.add(ConnectionSpec.CLEARTEXT));
+ assertTrue(set.add(allTlsVersions));
+ assertTrue(set.add(allCipherSuites));
+
+ assertTrue(set.remove(ConnectionSpec.MODERN_TLS));
+ assertTrue(set.remove(ConnectionSpec.COMPATIBLE_TLS));
+ assertTrue(set.remove(ConnectionSpec.CLEARTEXT));
+ assertTrue(set.remove(allTlsVersions));
+ assertTrue(set.remove(allCipherSuites));
+ assertTrue(set.isEmpty());
+ }
+
+ @Test public void allEnabledToString() throws Exception {
+ ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .allEnabledTlsVersions()
+ .allEnabledCipherSuites()
+ .build();
+ assertEquals("ConnectionSpec(cipherSuites=[all enabled], tlsVersions=[all enabled], "
+ + "supportsTlsExtensions=true)", connectionSpec.toString());
+ }
+
+ @Test public void simpleToString() throws Exception {
+ ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
+ .tlsVersions(TlsVersion.TLS_1_2)
+ .cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
+ .build();
+ assertEquals("ConnectionSpec(cipherSuites=[TLS_RSA_WITH_RC4_128_MD5], tlsVersions=[TLS_1_2], "
+ + "supportsTlsExtensions=true)", connectionSpec.toString());
+ }
+
+ private static <T> Set<T> set(T... values) {
+ return new LinkedHashSet<>(Arrays.asList(values));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
index 38a5de8..f72bd1a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/DelegatingSSLSocketFactory.java
@@ -35,51 +35,43 @@ public class DelegatingSSLSocketFactory extends SSLSocketFactory {
this.delegate = delegate;
}
- @Override
- public SSLSocket createSocket() throws IOException {
+ @Override public SSLSocket createSocket() throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket();
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(String host, int port) throws IOException, UnknownHostException {
+ @Override public SSLSocket createSocket(String host, int port) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(String host, int port, InetAddress localAddress, int localPort)
- throws IOException, UnknownHostException {
+ @Override public SSLSocket createSocket(
+ String host, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(InetAddress host, int port) throws IOException {
+ @Override public SSLSocket createSocket(InetAddress host, int port) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port);
return configureSocket(sslSocket);
}
- @Override
- public SSLSocket createSocket(InetAddress host, int port, InetAddress localAddress, int localPort)
- throws IOException {
+ @Override public SSLSocket createSocket(
+ InetAddress host, int port, InetAddress localAddress, int localPort) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(host, port, localAddress, localPort);
return configureSocket(sslSocket);
}
- @Override
- public String[] getDefaultCipherSuites() {
+ @Override public String[] getDefaultCipherSuites() {
return delegate.getDefaultCipherSuites();
}
- @Override
- public String[] getSupportedCipherSuites() {
+ @Override public String[] getSupportedCipherSuites() {
return delegate.getSupportedCipherSuites();
}
- @Override
- public SSLSocket createSocket(Socket socket, String host, int port, boolean autoClose)
- throws IOException {
+ @Override public SSLSocket createSocket(
+ Socket socket, String host, int port, boolean autoClose) throws IOException {
SSLSocket sslSocket = (SSLSocket) delegate.createSocket(socket, host, port, autoClose);
return configureSocket(sslSocket);
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
index 71deb6c..246dac2 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/HttpUrlTest.java
@@ -483,7 +483,12 @@ public final class HttpUrlTest {
assertEquals("http://host/#\u0080", url.toString());
assertEquals("\u0080", url.fragment());
assertEquals("\u0080", url.encodedFragment());
- assertEquals(new URI("http://host/#"), url.uri()); // Control characters may be stripped!
+ try {
+ url.uri();
+ fail();
+ } catch (IllegalStateException expected) {
+ // Possibly a bug in java.net.URI. Many non-ASCII code points work, this one doesn't!
+ }
}
@Test public void fragmentPercentEncodedNonAscii() throws Exception {
@@ -595,6 +600,8 @@ public final class HttpUrlTest {
HttpUrl.parse("http://host/%/b").pathSegments());
assertEquals(Arrays.asList("%"),
HttpUrl.parse("http://host/%").pathSegments());
+ assertEquals(Arrays.asList("%00"),
+ HttpUrl.parse("http://github.com/%%30%30").pathSegments());
}
@Test public void malformedUtf8Encoding() {
@@ -1051,46 +1058,19 @@ public final class HttpUrlTest {
assertEquals("http://host/#=[]:;%22~%7C?%23@%5E/$%25*", url.uri().toString());
}
- @Test public void toUriWithControlCharacters() throws Exception {
- // Percent-encoded in the path.
- assertEquals(new URI("http://host/a%00b"), HttpUrl.parse("http://host/a\u0000b").uri());
- assertEquals(new URI("http://host/a%C2%80b"), HttpUrl.parse("http://host/a\u0080b").uri());
- assertEquals(new URI("http://host/a%C2%9Fb"), HttpUrl.parse("http://host/a\u009fb").uri());
- // Percent-encoded in the query.
- assertEquals(new URI("http://host/?a%00b"), HttpUrl.parse("http://host/?a\u0000b").uri());
- assertEquals(new URI("http://host/?a%C2%80b"), HttpUrl.parse("http://host/?a\u0080b").uri());
- assertEquals(new URI("http://host/?a%C2%9Fb"), HttpUrl.parse("http://host/?a\u009fb").uri());
- // Stripped from the fragment.
- assertEquals(new URI("http://host/#a%00b"), HttpUrl.parse("http://host/#a\u0000b").uri());
- assertEquals(new URI("http://host/#ab"), HttpUrl.parse("http://host/#a\u0080b").uri());
- assertEquals(new URI("http://host/#ab"), HttpUrl.parse("http://host/#a\u009fb").uri());
- }
-
- @Test public void toUriWithSpaceCharacters() throws Exception {
- // Percent-encoded in the path.
- assertEquals(new URI("http://host/a%0Bb"), HttpUrl.parse("http://host/a\u000bb").uri());
- assertEquals(new URI("http://host/a%20b"), HttpUrl.parse("http://host/a b").uri());
- assertEquals(new URI("http://host/a%E2%80%89b"), HttpUrl.parse("http://host/a\u2009b").uri());
- assertEquals(new URI("http://host/a%E3%80%80b"), HttpUrl.parse("http://host/a\u3000b").uri());
- // Percent-encoded in the query.
- assertEquals(new URI("http://host/?a%0Bb"), HttpUrl.parse("http://host/?a\u000bb").uri());
- assertEquals(new URI("http://host/?a%20b"), HttpUrl.parse("http://host/?a b").uri());
- assertEquals(new URI("http://host/?a%E2%80%89b"), HttpUrl.parse("http://host/?a\u2009b").uri());
- assertEquals(new URI("http://host/?a%E3%80%80b"), HttpUrl.parse("http://host/?a\u3000b").uri());
- // Stripped from the fragment.
- assertEquals(new URI("http://host/#a%0Bb"), HttpUrl.parse("http://host/#a\u000bb").uri());
- assertEquals(new URI("http://host/#a%20b"), HttpUrl.parse("http://host/#a b").uri());
- assertEquals(new URI("http://host/#ab"), HttpUrl.parse("http://host/#a\u2009b").uri());
- assertEquals(new URI("http://host/#ab"), HttpUrl.parse("http://host/#a\u3000b").uri());
- }
-
- @Test public void toUriWithNonHexPercentEscape() throws Exception {
- assertEquals(new URI("http://host/%25xx"), HttpUrl.parse("http://host/%xx").uri());
- }
-
- @Test public void toUriWithTruncatedPercentEscape() throws Exception {
- assertEquals(new URI("http://host/%25a"), HttpUrl.parse("http://host/%a").uri());
- assertEquals(new URI("http://host/%25"), HttpUrl.parse("http://host/%").uri());
+ @Test public void toUriWithMalformedPercentEscape() throws Exception {
+ HttpUrl url = new HttpUrl.Builder()
+ .scheme("http")
+ .host("host")
+ .encodedPath("/%xx")
+ .build();
+ assertEquals("http://host/%xx", url.toString());
+ try {
+ url.uri();
+ fail();
+ } catch (IllegalStateException expected) {
+ assertEquals("not valid as a java.net.URI: http://host/%xx", expected.getMessage());
+ }
}
@Test public void fromJavaNetUrl() throws Exception {
@@ -1099,9 +1079,6 @@ public final class HttpUrlTest {
assertEquals("http://username:password@host/path?query#fragment", httpUrl.toString());
}
- // ANDROID-BEGIN
- @Ignore // Android's URL implementation does not support mailto:
- // ANDROID-END
@Test public void fromJavaNetUrlUnsupportedScheme() throws Exception {
URL javaNetUrl = new URL("mailto:user@example.com");
assertEquals(null, HttpUrl.get(javaNetUrl));
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
index 054343c..e70c908 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/InterceptorTest.java
@@ -210,6 +210,35 @@ public final class InterceptorTest {
assertEquals("abcabcabc", response.body().string());
}
+ @Test public void networkInterceptorsCanChangeRequestMethodFromGetToPost() throws Exception {
+ server.enqueue(new MockResponse());
+
+ client.networkInterceptors().add(new Interceptor() {
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request originalRequest = chain.request();
+ MediaType mediaType = MediaType.parse("text/plain");
+ RequestBody body = RequestBody.create(mediaType, "abc");
+ return chain.proceed(originalRequest.newBuilder()
+ .method("POST", body)
+ .header("Content-Type", mediaType.toString())
+ .header("Content-Length", Long.toString(body.contentLength()))
+ .build());
+ }
+ });
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .get()
+ .build();
+
+ client.newCall(request).execute();
+
+ RecordedRequest recordedRequest = server.takeRequest();
+ assertEquals("POST", recordedRequest.getMethod());
+ assertEquals("abc", recordedRequest.getBody().readUtf8());
+ }
+
@Test public void applicationInterceptorsRewriteRequestToServer() throws Exception {
rewriteRequestToServer(client.interceptors());
}
@@ -362,7 +391,8 @@ public final class InterceptorTest {
client.interceptors().add(new Interceptor() {
@Override public Response intercept(Chain chain) throws IOException {
- chain.proceed(chain.request());
+ Response response1 = chain.proceed(chain.request());
+ response1.body().close();
return chain.proceed(chain.request());
}
});
@@ -468,14 +498,6 @@ public final class InterceptorTest {
}
}
- @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
- interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
- }
-
- @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
- interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
- }
-
@Test public void networkInterceptorModifiedRequestIsReturned() throws IOException {
server.enqueue(new MockResponse());
@@ -500,6 +522,14 @@ public final class InterceptorTest {
assertEquals("intercepted request", response.networkResponse().request().header("User-Agent"));
}
+ @Test public void applicationInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
+ interceptorThrowsRuntimeExceptionAsynchronous(client.interceptors());
+ }
+
+ @Test public void networkInterceptorThrowsRuntimeExceptionAsynchronous() throws Exception {
+ interceptorThrowsRuntimeExceptionAsynchronous(client.networkInterceptors());
+ }
+
/**
* When an interceptor throws an unexpected exception, asynchronous callers are left hanging. The
* exception goes to the uncaught exception handler.
@@ -525,6 +555,57 @@ public final class InterceptorTest {
assertEquals("boom!", executor.takeException().getMessage());
}
+ @Test public void applicationInterceptorReturnsNull() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ chain.proceed(chain.request());
+ return null;
+ }
+ };
+ client.interceptors().add(interceptor);
+
+ ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
+ client.setDispatcher(new Dispatcher(executor));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (NullPointerException expected) {
+ assertEquals("application interceptor " + interceptor
+ + " returned null", expected.getMessage());
+ }
+ }
+
+ @Test public void networkInterceptorReturnsNull() throws Exception {
+ server.enqueue(new MockResponse());
+
+ Interceptor interceptor = new Interceptor() {
+ @Override public Response intercept(Chain chain) throws IOException {
+ chain.proceed(chain.request());
+ return null;
+ }
+ };
+ client.networkInterceptors().add(interceptor);
+
+ ExceptionCatchingExecutor executor = new ExceptionCatchingExecutor();
+ client.setDispatcher(new Dispatcher(executor));
+
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ try {
+ client.newCall(request).execute();
+ fail();
+ } catch (NullPointerException expected) {
+ assertEquals("network interceptor " + interceptor + " returned null", expected.getMessage());
+ }
+ }
+
private RequestBody uppercase(final RequestBody original) {
return new RequestBody() {
@Override public MediaType contentType() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
index 9d65147..f2447ec 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RecordingCallback.java
@@ -20,7 +20,6 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.TimeUnit;
-import okio.Buffer;
/**
* Records received HTTP responses so they can be later retrieved by tests.
@@ -36,11 +35,8 @@ public class RecordingCallback implements Callback {
}
@Override public synchronized void onResponse(Response response) throws IOException {
- Buffer buffer = new Buffer();
- ResponseBody body = response.body();
- body.source().readAll(buffer);
-
- responses.add(new RecordedResponse(response.request(), response, null, buffer.readUtf8(), null));
+ String body = response.body().string();
+ responses.add(new RecordedResponse(response.request(), response, null, body, null));
notifyAll();
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
index 47c6010..39da500 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/RequestTest.java
@@ -177,22 +177,13 @@ public final class RequestTest {
@Test public void headerForbidsControlCharacters() throws Exception {
assertForbiddenHeader(null);
assertForbiddenHeader("\u0000");
- // Workaround for http://b/26422335 , http://b/26889631
- // assertForbiddenHeader("\n");
- assertForbiddenHeader("a\nb");
- assertForbiddenHeader("\nb");
- // assertForbiddenHeader("\r");
- assertForbiddenHeader("a\rb");
- assertForbiddenHeader("\rb");
- // End of Android modification.
+ assertForbiddenHeader("\r");
+ assertForbiddenHeader("\n");
assertForbiddenHeader("\t");
assertForbiddenHeader("\u001f");
assertForbiddenHeader("\u007f");
-
- // ANDROID-BEGIN Workaround for http://b/28867041
- // assertForbiddenHeader("\u0080");
- // assertForbiddenHeader("\ud83c\udf69");
- // ANDROID-END
+ assertForbiddenHeader("\u0080");
+ assertForbiddenHeader("\ud83c\udf69");
}
private void assertForbiddenHeader(String s) {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
index e2a5532..854ddaf 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxy.java
@@ -41,6 +41,8 @@ import okio.Okio;
* See <a href="https://www.ietf.org/rfc/rfc1928.txt">RFC 1928</a>.
*/
public final class SocksProxy {
+ public final String HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS = "onlyProxyCanResolveMe.org";
+
private static final int VERSION_5 = 5;
private static final int METHOD_NONE = 0xff;
private static final int METHOD_NO_AUTHENTICATION_REQUIRED = 0;
@@ -156,7 +158,10 @@ public final class SocksProxy {
case ADDRESS_TYPE_DOMAIN_NAME:
int domainNameLength = fromSource.readByte() & 0xff;
String domainName = fromSource.readUtf8(domainNameLength);
- toAddress = InetAddress.getByName(domainName);
+ // Resolve HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS to localhost.
+ toAddress = domainName.equalsIgnoreCase(HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
+ ? InetAddress.getLoopbackAddress()
+ : InetAddress.getByName(domainName);
break;
default:
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
index 377ff83..0d6e9fd 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SocksProxyTest.java
@@ -85,4 +85,23 @@ public final class SocksProxyTest {
assertEquals(1, socksProxy.connectionCount());
}
+
+ @Test public void checkRemoteDNSResolve() throws Exception {
+ // This testcase will fail if the target is resolved locally instead of through the proxy.
+ server.enqueue(new MockResponse().setBody("abc"));
+
+ OkHttpClient client = new OkHttpClient()
+ .setProxy(socksProxy.proxy());
+
+ HttpUrl url = server.url("/")
+ .newBuilder()
+ .host(socksProxy.HOSTNAME_THAT_ONLY_THE_PROXY_KNOWS)
+ .build();
+
+ Request request = new Request.Builder().url(url).build();
+ Response response1 = client.newCall(request).execute();
+ assertEquals("abc", response1.body().string());
+
+ assertEquals(1, socksProxy.connectionCount());
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
index 3598da0..d638deb 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/URLConnectionTest.java
@@ -17,9 +17,10 @@
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.RecordingAuthenticator;
import com.squareup.okhttp.internal.RecordingOkAuthenticator;
-import com.squareup.okhttp.internal.SingleInetAddressNetwork;
+import com.squareup.okhttp.internal.SingleInetAddressDns;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.Version;
@@ -47,6 +48,7 @@ import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
+import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
@@ -226,6 +228,7 @@ public final class URLConnectionTest {
assertEquals("d", connection.getHeaderField(1));
assertEquals("A", connection.getHeaderFieldKey(2));
assertEquals("e", connection.getHeaderField(2));
+ connection.getInputStream().close();
}
@Test public void serverSendsInvalidResponseHeaders() throws Exception {
@@ -318,6 +321,7 @@ public final class URLConnectionTest {
server.enqueue(new MockResponse().setBody("A"));
connection = client.open(server.getUrl("/"));
assertNull(connection.getErrorStream());
+ connection.getInputStream().close();
}
@Test public void getErrorStreamOnUnsuccessfulRequest() throws Exception {
@@ -335,8 +339,13 @@ public final class URLConnectionTest {
server.enqueue(response);
server.enqueue(response);
- assertContent("ABCDE", client.open(server.getUrl("/")), 5);
- assertContent("ABCDE", client.open(server.getUrl("/")), 5);
+ HttpURLConnection c1 = client.open(server.getUrl("/"));
+ assertContent("ABCDE", c1, 5);
+ HttpURLConnection c2 = client.open(server.getUrl("/"));
+ assertContent("ABCDE", c2, 5);
+
+ c1.getInputStream().close();
+ c2.getInputStream().close();
}
// Check that we recognize a few basic mime types by extension.
@@ -607,7 +616,7 @@ public final class URLConnectionTest {
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
suppressTlsFallbackScsv(client.client());
- Internal.instance.setNetwork(client.client(), new SingleInetAddressNetwork());
+ client.client().setDns(new SingleInetAddressDns());
client.client().setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/foo"));
@@ -873,7 +882,7 @@ public final class URLConnectionTest {
// Configure a single IP address for the host and a single configuration, so we only need one
// failure to fail permanently.
- Internal.instance.setNetwork(client.client(), new SingleInetAddressNetwork());
+ client.client().setDns(new SingleInetAddressDns());
client.client().setSslSocketFactory(sslContext.getSocketFactory());
client.client().setConnectionSpecs(Util.immutableList(ConnectionSpec.MODERN_TLS));
client.client().setHostnameVerifier(new RecordingHostnameVerifier());
@@ -916,8 +925,8 @@ public final class URLConnectionTest {
RecordedRequest connect = server.takeRequest();
assertNull(connect.getHeader("Private"));
- assertEquals("bar", connect.getHeader("Proxy-Authorization"));
- assertEquals("baz", connect.getHeader("User-Agent"));
+ assertNull(connect.getHeader("Proxy-Authorization"));
+ assertEquals(Version.userAgent(), connect.getHeader("User-Agent"));
assertEquals("android.com", connect.getHeader("Host"));
assertEquals("Keep-Alive", connect.getHeader("Proxy-Connection"));
@@ -1013,6 +1022,7 @@ public final class URLConnectionTest {
fail("Expected a connection closed exception");
} catch (IOException expected) {
}
+ in.close();
}
@Test public void disconnectBeforeConnect() throws IOException {
@@ -1075,6 +1085,7 @@ public final class URLConnectionTest {
} catch (IOException expected) {
}
assertEquals("FGHIJKLMNOPQRSTUVWXYZ", readAscii(in, Integer.MAX_VALUE));
+ in.close();
assertContent("ABCDEFGHIJKLMNOPQRSTUVWXYZ", client.open(server.getUrl("/")));
}
@@ -1098,6 +1109,7 @@ public final class URLConnectionTest {
assertEquals(401, conn.getResponseCode());
assertEquals(401, conn.getResponseCode());
assertEquals(1, server.getRequestCount());
+ conn.getErrorStream().close();
}
@Test public void nonHexChunkSize() throws IOException {
@@ -1111,6 +1123,7 @@ public final class URLConnectionTest {
fail();
} catch (IOException e) {
}
+ connection.getInputStream().close();
}
@Test public void malformedChunkSize() throws IOException {
@@ -1123,6 +1136,8 @@ public final class URLConnectionTest {
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail();
} catch (IOException e) {
+ } finally {
+ connection.getInputStream().close();
}
}
@@ -1146,6 +1161,8 @@ public final class URLConnectionTest {
readAscii(connection.getInputStream(), Integer.MAX_VALUE);
fail();
} catch (IOException e) {
+ } finally {
+ connection.getInputStream().close();
}
}
@@ -1276,7 +1293,7 @@ public final class URLConnectionTest {
HttpURLConnection connection = client.open(server.getUrl("/"));
assertContent("{}", connection);
- assertEquals(0, client.client().getConnectionPool().getConnectionCount());
+ assertEquals(0, client.client().getConnectionPool().getIdleConnectionCount());
}
@Test public void earlyDisconnectDoesntHarmPoolingWithChunkedEncoding() throws Exception {
@@ -1348,6 +1365,7 @@ public final class URLConnectionTest {
OutputStream outputStream = connection.getOutputStream();
outputStream.write(body.getBytes("US-ASCII"));
assertEquals(200, connection.getResponseCode());
+ connection.getInputStream().close();
RecordedRequest request = server.takeRequest();
assertEquals(body, request.getBody().readUtf8());
@@ -1501,7 +1519,8 @@ public final class URLConnectionTest {
int responseCode = proxy ? 407 : 401;
RecordingAuthenticator authenticator = new RecordingAuthenticator(null);
Authenticator.setDefault(authenticator);
- MockResponse pleaseAuthenticate = new MockResponse().setResponseCode(responseCode)
+ MockResponse pleaseAuthenticate = new MockResponse()
+ .setResponseCode(responseCode)
.addHeader(authHeader)
.setBody("Please authenticate.");
server.enqueue(pleaseAuthenticate);
@@ -1513,6 +1532,7 @@ public final class URLConnectionTest {
connection = client.open(server.getUrl("/"));
}
assertEquals(responseCode, connection.getResponseCode());
+ connection.getErrorStream().close();
return authenticator.calls;
}
@@ -1958,7 +1978,7 @@ public final class URLConnectionTest {
assertContent("This is the 2nd server!", client.open(server.getUrl("/a")));
- assertEquals(Arrays.asList(server.getUrl("/a").toURI(), server2.getUrl("/b").toURI()),
+ assertEquals(Arrays.asList(server.getUrl("/").toURI(), server2.getUrl("/").toURI()),
proxySelectionRequests);
}
@@ -2180,9 +2200,9 @@ public final class URLConnectionTest {
@Test public void httpsWithCustomTrustManager() throws Exception {
RecordingHostnameVerifier hostnameVerifier = new RecordingHostnameVerifier();
- RecordingTrustManager trustManager = new RecordingTrustManager();
+ RecordingTrustManager trustManager = new RecordingTrustManager(sslContext);
SSLContext sc = SSLContext.getInstance("TLS");
- sc.init(null, new TrustManager[] { trustManager }, new java.security.SecureRandom());
+ sc.init(null, new TrustManager[] {trustManager}, new SecureRandom());
client.client().setHostnameVerifier(hostnameVerifier);
client.client().setSslSocketFactory(sc.getSocketFactory());
@@ -2196,8 +2216,7 @@ public final class URLConnectionTest {
assertContent("DEF", client.open(url));
assertContent("GHI", client.open(url));
- assertEquals(Arrays.asList("verify " + server.getHostName()),
- hostnameVerifier.calls);
+ assertEquals(Arrays.asList("verify " + server.getHostName()), hostnameVerifier.calls);
assertEquals(Arrays.asList("checkServerTrusted [CN=" + server.getHostName() + " 1]"),
trustManager.calls);
}
@@ -2222,6 +2241,7 @@ public final class URLConnectionTest {
fail();
} catch (SocketTimeoutException expected) {
}
+ in.close();
}
/** Confirm that an unacknowledged write times out. */
@@ -2490,6 +2510,7 @@ public final class URLConnectionTest {
} catch (NullPointerException expected) {
}
assertNull(connection.getContent(new Class[] { getClass() }));
+ connection.getInputStream().close();
}
@Test public void getOutputStreamOnGetFails() throws Exception {
@@ -2500,6 +2521,7 @@ public final class URLConnectionTest {
fail();
} catch (ProtocolException expected) {
}
+ connection.getInputStream().close();
}
@Test public void getOutputAfterGetInputStreamFails() throws Exception {
@@ -2528,6 +2550,7 @@ public final class URLConnectionTest {
fail();
} catch (IllegalStateException expected) {
}
+ connection.getInputStream().close();
}
@Test public void clientSendsContentLength() throws Exception {
@@ -2540,24 +2563,28 @@ public final class URLConnectionTest {
assertEquals("A", readAscii(connection.getInputStream(), Integer.MAX_VALUE));
RecordedRequest request = server.takeRequest();
assertEquals("3", request.getHeader("Content-Length"));
+ connection.getInputStream().close();
}
@Test public void getContentLengthConnects() throws Exception {
server.enqueue(new MockResponse().setBody("ABC"));
connection = client.open(server.getUrl("/"));
assertEquals(3, connection.getContentLength());
+ connection.getInputStream().close();
}
@Test public void getContentTypeConnects() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Type: text/plain").setBody("ABC"));
connection = client.open(server.getUrl("/"));
assertEquals("text/plain", connection.getContentType());
+ connection.getInputStream().close();
}
@Test public void getContentEncodingConnects() throws Exception {
server.enqueue(new MockResponse().addHeader("Content-Encoding: identity").setBody("ABC"));
connection = client.open(server.getUrl("/"));
assertEquals("identity", connection.getContentEncoding());
+ connection.getInputStream().close();
}
// http://b/4361656
@@ -2787,6 +2814,7 @@ public final class URLConnectionTest {
connection = client.open(server.getUrl("/"));
connection.getResponseCode();
assertEquals("A", connection.getHeaderField(""));
+ connection.getInputStream().close();
}
@Test public void requestHeaderValidationIsStrict() throws Exception {
@@ -2811,14 +2839,11 @@ public final class URLConnectionTest {
fail();
} catch (IllegalArgumentException expected) {
}
-
- // ANDROID-BEGIN Disabled for http://b/28867041
- // try {
- // connection.addRequestProperty("Name", "\u2615\ufe0f");
- // fail();
- // } catch (IllegalArgumentException expected) {
- // }
- // ANDROID-END
+ try {
+ connection.addRequestProperty("Name", "\u2615\ufe0f");
+ fail();
+ } catch (IllegalArgumentException expected) {
+ }
}
@Test public void responseHeaderParsingIsLenient() throws Exception {
@@ -3370,9 +3395,14 @@ public final class URLConnectionTest {
private static class RecordingTrustManager implements X509TrustManager {
private final List<String> calls = new ArrayList<String>();
+ private final X509TrustManager delegate;
+
+ public RecordingTrustManager(SSLContext sslContext) {
+ this.delegate = Platform.get().trustManager(sslContext.getSocketFactory());
+ }
public X509Certificate[] getAcceptedIssuers() {
- return new X509Certificate[] { };
+ return delegate.getAcceptedIssuers();
}
public void checkClientTrusted(X509Certificate[] chain, String authType)
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
index 4934b42..759ac5d 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressNetwork.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/DoubleInetAddressDns.java
@@ -15,16 +15,19 @@
*/
package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Dns;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
/**
* A network that always resolves two IP addresses per host. Use this when testing route selection
* fallbacks to guarantee that a fallback address is available.
*/
-public class DoubleInetAddressNetwork implements Network {
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- InetAddress[] allInetAddresses = Network.DEFAULT.resolveInetAddresses(host);
- return new InetAddress[] { allInetAddresses[0], allInetAddresses[0] };
+public class DoubleInetAddressDns implements Dns {
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
+ return Arrays.asList(addresses.get(0), addresses.get(0));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
index beb48cb..43cbe63 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressNetwork.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/SingleInetAddressDns.java
@@ -15,17 +15,20 @@
*/
package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Dns;
import java.net.InetAddress;
import java.net.UnknownHostException;
+import java.util.Collections;
+import java.util.List;
/**
* A network that resolves only one IP address per host. Use this when testing
* route selection fallbacks to prevent the host machine's various IP addresses
* from interfering.
*/
-public class SingleInetAddressNetwork implements Network {
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- InetAddress[] allInetAddresses = Network.DEFAULT.resolveInetAddresses(host);
- return new InetAddress[] { allInetAddresses[0] };
+public class SingleInetAddressDns implements Dns {
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ List<InetAddress> addresses = Dns.SYSTEM.lookup(hostname);
+ return Collections.singletonList(addresses.get(0));
}
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
index 24c512d..1e00b4b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Http2ConnectionTest.java
@@ -443,7 +443,8 @@ public final class Http2ConnectionTest {
String longString = repeat('a', Http2.INITIAL_MAX_FRAME_SIZE + 1);
Socket socket = peer.openSocket();
- FramedConnection connection = new FramedConnection.Builder(true, socket)
+ FramedConnection connection = new FramedConnection.Builder(true)
+ .socket(socket)
.pushObserver(IGNORE)
.protocol(HTTP_2.getProtocol())
.build();
@@ -488,7 +489,8 @@ public final class Http2ConnectionTest {
private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
throws IOException {
- return new FramedConnection.Builder(true, peer.openSocket())
+ return new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
.pushObserver(IGNORE)
.protocol(variant.getProtocol());
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
index 91ba56c..7947c03 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverHttp2Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverHttp2Test.java
@@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.PushPromise;
import com.squareup.okhttp.mockwebserver.RecordedRequest;
+import java.net.HttpURLConnection;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -78,4 +79,36 @@ public class HttpOverHttp2Test extends HttpOverSpdyTest {
assertEquals("HEAD /foo/bar HTTP/1.1", pushedRequest.getRequestLine());
assertEquals("bar", pushedRequest.getHeader("foo"));
}
+
+ /**
+ * Push a setting that permits up to 2 concurrent streams, then make 3 concurrent requests and
+ * confirm that the third concurrent request prepared a new connection.
+ */
+ @Test public void settingsLimitsMaxConcurrentStreams() throws Exception {
+ Settings settings = new Settings();
+ settings.set(Settings.MAX_CONCURRENT_STREAMS, 0, 2);
+
+ // Read & write a full request to confirm settings are accepted.
+ server.enqueue(new MockResponse().withSettings(settings));
+ HttpURLConnection settingsConnection = client.open(server.getUrl("/"));
+ assertContent("", settingsConnection, Integer.MAX_VALUE);
+
+ server.enqueue(new MockResponse().setBody("ABC"));
+ server.enqueue(new MockResponse().setBody("DEF"));
+ server.enqueue(new MockResponse().setBody("GHI"));
+
+ HttpURLConnection connection1 = client.open(server.getUrl("/"));
+ connection1.connect();
+ HttpURLConnection connection2 = client.open(server.getUrl("/"));
+ connection2.connect();
+ HttpURLConnection connection3 = client.open(server.getUrl("/"));
+ connection3.connect();
+ assertContent("ABC", connection1, Integer.MAX_VALUE);
+ assertContent("DEF", connection2, Integer.MAX_VALUE);
+ assertContent("GHI", connection3, Integer.MAX_VALUE);
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // Settings connection.
+ assertEquals(1, server.takeRequest().getSequenceNumber()); // Reuse settings connection.
+ assertEquals(2, server.takeRequest().getSequenceNumber()); // Reuse settings connection.
+ assertEquals(0, server.takeRequest().getSequenceNumber()); // New connection!
+ }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
index 4020bf4..5e9bf61 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdy3Test.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdy3Test.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.Protocol;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
index 2d52eee..1f7d999 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HttpOverSpdyTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/HttpOverSpdyTest.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal.http;
+package com.squareup.okhttp.internal.framed;
import com.squareup.okhttp.Cache;
import com.squareup.okhttp.ConnectionPool;
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
index 26d4986..752e92b 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/framed/Spdy3ConnectionTest.java
@@ -150,15 +150,18 @@ public final class Spdy3ConnectionTest {
// play it back
final AtomicInteger receiveCount = new AtomicInteger();
- IncomingStreamHandler handler = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
+ FramedConnection.Listener handler = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
receiveCount.incrementAndGet();
assertEquals(pushHeaders, stream.getRequestHeaders());
assertEquals(null, stream.getErrorCode());
stream.reply(headerEntries("b", "banana"), true);
}
};
- new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
+ new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
+ .listener(handler)
+ .build();
// verify the peer received what was expected
MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -178,14 +181,14 @@ public final class Spdy3ConnectionTest {
// play it back
final AtomicInteger receiveCount = new AtomicInteger();
- IncomingStreamHandler handler = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
+ FramedConnection.Listener listener = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
stream.reply(headerEntries("b", "banana"), false);
receiveCount.incrementAndGet();
}
};
- connectionBuilder(peer, SPDY3).handler(handler).build();
+ connectionBuilder(peer, SPDY3).listener(listener).build();
// verify the peer received what was expected
MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -254,7 +257,7 @@ public final class Spdy3ConnectionTest {
@Test public void serverSendsSettingsToClient() throws Exception {
// write the mocking script
- Settings settings = new Settings();
+ final Settings settings = new Settings();
settings.set(Settings.MAX_CONCURRENT_STREAMS, PERSIST_VALUE, 10);
peer.sendFrame().settings(settings);
peer.sendFrame().ping(false, 2, 0);
@@ -262,12 +265,24 @@ public final class Spdy3ConnectionTest {
peer.play();
// play it back
- FramedConnection connection = connection(peer, SPDY3);
+ final AtomicInteger maxConcurrentStreams = new AtomicInteger();
+ FramedConnection.Listener listener = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
+ throw new AssertionError();
+ }
+ @Override public void onSettings(FramedConnection connection) {
+ maxConcurrentStreams.set(connection.maxConcurrentStreams());
+ }
+ };
+ FramedConnection connection = connectionBuilder(peer, SPDY3)
+ .listener(listener)
+ .build();
peer.takeFrame(); // Guarantees that the peer Settings frame has been processed.
synchronized (connection) {
assertEquals(10, connection.peerSettings.getMaxConcurrentStreams(-1));
}
+ assertEquals(10, maxConcurrentStreams.get());
}
@Test public void multipleSettingsFramesAreMerged() throws Exception {
@@ -613,15 +628,18 @@ public final class Spdy3ConnectionTest {
// play it back
final AtomicInteger receiveCount = new AtomicInteger();
- IncomingStreamHandler handler = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
+ FramedConnection.Listener listener = new FramedConnection.Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
receiveCount.incrementAndGet();
assertEquals(headerEntries("a", "android"), stream.getRequestHeaders());
assertEquals(null, stream.getErrorCode());
stream.reply(headerEntries("c", "cola"), true);
}
};
- new FramedConnection.Builder(true, peer.openSocket()).handler(handler).build();
+ new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
+ .listener(listener)
+ .build();
// verify the peer received what was expected
MockSpdyPeer.InFrame reply = peer.takeFrame();
@@ -1315,7 +1333,8 @@ public final class Spdy3ConnectionTest {
String longString = ByteString.of(randomBytes(2048)).base64();
Socket socket = peer.openSocket();
- FramedConnection connection = new FramedConnection.Builder(true, socket)
+ FramedConnection connection = new FramedConnection.Builder(true)
+ .socket(socket)
.protocol(SPDY3.getProtocol())
.build();
socket.shutdownOutput();
@@ -1343,7 +1362,8 @@ public final class Spdy3ConnectionTest {
private FramedConnection.Builder connectionBuilder(MockSpdyPeer peer, Variant variant)
throws IOException {
- return new FramedConnection.Builder(true, peer.openSocket())
+ return new FramedConnection.Builder(true)
+ .socket(peer.openSocket())
.protocol(variant.getProtocol());
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java
new file mode 100644
index 0000000..03d4347
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/FakeDns.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Dns;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+
+public final class FakeDns implements Dns {
+ private List<String> requestedHosts = new ArrayList<>();
+ private List<InetAddress> addresses = Collections.emptyList();
+
+ /** Sets the addresses to be returned by this fake DNS service. */
+ public FakeDns addresses(List<InetAddress> addresses) {
+ this.addresses = new ArrayList<>(addresses);
+ return this;
+ }
+
+ /** Sets the service to throw when a hostname is requested. */
+ public FakeDns unknownHost() {
+ this.addresses = Collections.emptyList();
+ return this;
+ }
+
+ public InetAddress address(int index) {
+ return addresses.get(index);
+ }
+
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ requestedHosts.add(hostname);
+ if (addresses.isEmpty()) throw new UnknownHostException();
+ return addresses;
+ }
+
+ public void assertRequests(String... expectedHosts) {
+ assertEquals(Arrays.asList(expectedHosts), requestedHosts);
+ requestedHosts.clear();
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
index 1f5ad6d..ea6e024 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/HeadersTest.java
@@ -26,7 +26,6 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-
import org.junit.Test;
import static com.squareup.okhttp.TestUtil.headerEntries;
@@ -42,24 +41,20 @@ public final class HeadersTest {
":status", "200 OK",
":version", "HTTP/1.1");
Request request = new Request.Builder().url("http://square.com/").build();
- Response response =
- FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+ Response response = Http2xStream.readSpdy3HeadersList(headerBlock).request(request).build();
Headers headers = response.headers();
- assertEquals(4, headers.size());
+ assertEquals(3, headers.size());
assertEquals(Protocol.SPDY_3, response.protocol());
assertEquals(200, response.code());
assertEquals("OK", response.message());
assertEquals("no-cache, no-store", headers.get("cache-control"));
assertEquals("Cookie2", headers.get("set-cookie"));
- assertEquals(Protocol.SPDY_3.toString(), headers.get(OkHeaders.SELECTED_PROTOCOL));
- assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
- assertEquals("cache-control", headers.name(1));
- assertEquals("no-cache, no-store", headers.value(1));
+ assertEquals("cache-control", headers.name(0));
+ assertEquals("no-cache, no-store", headers.value(0));
+ assertEquals("set-cookie", headers.name(1));
+ assertEquals("Cookie1", headers.value(1));
assertEquals("set-cookie", headers.name(2));
- assertEquals("Cookie1", headers.value(2));
- assertEquals("set-cookie", headers.name(3));
- assertEquals("Cookie2", headers.value(3));
+ assertEquals("Cookie2", headers.value(2));
assertNull(headers.get(":status"));
assertNull(headers.get(":version"));
}
@@ -70,12 +65,9 @@ public final class HeadersTest {
":version", "HTTP/1.1",
"connection", "close");
Request request = new Request.Builder().url("http://square.com/").build();
- Response response =
- FramedTransport.readNameValueBlock(headerBlock, Protocol.SPDY_3).request(request).build();
+ Response response = Http2xStream.readSpdy3HeadersList(headerBlock).request(request).build();
Headers headers = response.headers();
- assertEquals(1, headers.size());
- assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.SPDY_3.toString(), headers.value(0));
+ assertEquals(0, headers.size());
}
@Test public void readNameValueBlockDropsForbiddenHeadersHttp2() throws IOException {
@@ -84,15 +76,14 @@ public final class HeadersTest {
":version", "HTTP/1.1",
"connection", "close");
Request request = new Request.Builder().url("http://square.com/").build();
- Response response = FramedTransport.readNameValueBlock(headerBlock, Protocol.HTTP_2)
- .request(request).build();
+ Response response = Http2xStream.readHttp2HeadersList(headerBlock).request(request).build();
Headers headers = response.headers();
assertEquals(1, headers.size());
- assertEquals(OkHeaders.SELECTED_PROTOCOL, headers.name(0));
- assertEquals(Protocol.HTTP_2.toString(), headers.value(0));
+ assertEquals(":version", headers.name(0));
+ assertEquals("HTTP/1.1", headers.value(0));
}
- @Test public void toNameValueBlock() {
+ @Test public void spdy3HeadersList() {
Request request = new Request.Builder()
.url("http://square.com/")
.header("cache-control", "no-cache, no-store")
@@ -100,8 +91,7 @@ public final class HeadersTest {
.addHeader("set-cookie", "Cookie2")
.header(":status", "200 OK")
.build();
- List<Header> headerBlock =
- FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1");
+ List<Header> headerBlock = Http2xStream.spdy3HeadersList(request);
List<Header> expected = headerEntries(
":method", "GET",
":path", "/",
@@ -114,7 +104,7 @@ public final class HeadersTest {
assertEquals(expected, headerBlock);
}
- @Test public void toNameValueBlockDropsForbiddenHeadersSpdy3() {
+ @Test public void spdy3HeadersListDropsForbiddenHeadersSpdy3() {
Request request = new Request.Builder()
.url("http://square.com/")
.header("Connection", "close")
@@ -126,10 +116,10 @@ public final class HeadersTest {
":version", "HTTP/1.1",
":host", "square.com",
":scheme", "http");
- assertEquals(expected, FramedTransport.writeNameValueBlock(request, Protocol.SPDY_3, "HTTP/1.1"));
+ assertEquals(expected, Http2xStream.spdy3HeadersList(request));
}
- @Test public void toNameValueBlockDropsForbiddenHeadersHttp2() {
+ @Test public void http2HeadersListDropsForbiddenHeadersHttp2() {
Request request = new Request.Builder()
.url("http://square.com/")
.header("Connection", "upgrade")
@@ -140,8 +130,7 @@ public final class HeadersTest {
":path", "/",
":authority", "square.com",
":scheme", "http");
- assertEquals(expected,
- FramedTransport.writeNameValueBlock(request, Protocol.HTTP_2, "HTTP/1.1"));
+ assertEquals(expected, Http2xStream.http2HeadersList(request));
}
@Test public void ofTrims() {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index bb8d082..8560e0f 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
@@ -17,14 +17,9 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Authenticator;
-import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.ConnectionSpec;
-import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.Network;
import com.squareup.okhttp.internal.RouteDatabase;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
@@ -57,14 +52,14 @@ public final class RouteSelectorTest {
ConnectionSpec.CLEARTEXT);
private static final int proxyAPort = 1001;
- private static final String proxyAHost = "proxyA";
+ private static final String proxyAHost = "proxya";
private static final Proxy proxyA =
- new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyAHost, proxyAPort));
+ new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyAHost, proxyAPort));
private static final int proxyBPort = 1002;
- private static final String proxyBHost = "proxyB";
+ private static final String proxyBHost = "proxyb";
private static final Proxy proxyB =
- new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyBHost, proxyBPort));
- private String uriHost = "hostA";
+ new Proxy(Proxy.Type.HTTP, InetSocketAddress.createUnresolved(proxyBHost, proxyBPort));
+ private String uriHost = "hosta";
private int uriPort = 1003;
private SocketFactory socketFactory;
@@ -76,43 +71,20 @@ public final class RouteSelectorTest {
private final List<Protocol> protocols = Arrays.asList(Protocol.HTTP_1_1);
private final FakeDns dns = new FakeDns();
private final RecordingProxySelector proxySelector = new RecordingProxySelector();
- private OkHttpClient client;
- private RouteDatabase routeDatabase;
- private Request httpRequest;
- private Request httpsRequest;
+ private RouteDatabase routeDatabase = new RouteDatabase();
@Before public void setUp() throws Exception {
socketFactory = SocketFactory.getDefault();
hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
-
- client = new OkHttpClient()
- .setAuthenticator(authenticator)
- .setProxySelector(proxySelector)
- .setSocketFactory(socketFactory)
- .setSslSocketFactory(sslSocketFactory)
- .setHostnameVerifier(hostnameVerifier)
- .setProtocols(protocols)
- .setConnectionSpecs(connectionSpecs)
- .setConnectionPool(ConnectionPool.getDefault());
- Internal.instance.setNetwork(client, dns);
-
- routeDatabase = Internal.instance.routeDatabase(client);
-
- httpRequest = new Request.Builder()
- .url("http://" + uriHost + ":" + uriPort + "/path")
- .build();
- httpsRequest = new Request.Builder()
- .url("https://" + uriHost + ":" + uriPort + "/path")
- .build();
}
@Test public void singleRoute() throws Exception {
Address address = httpAddress();
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -125,14 +97,14 @@ public final class RouteSelectorTest {
@Test public void singleRouteReturnsFailedRoute() throws Exception {
Address address = httpAddress();
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
+ dns.addresses(makeFakeAddresses(255, 1));
Route route = routeSelector.next();
routeDatabase.failed(route);
- routeSelector = RouteSelector.get(address, httpRequest, client);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ routeSelector = new RouteSelector(address, routeDatabase);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
assertFalse(routeSelector.hasNext());
try {
routeSelector.next();
@@ -142,15 +114,14 @@ public final class RouteSelectorTest {
}
@Test public void explicitProxyTriesThatProxysAddressesOnly() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator,
- proxyA, protocols, connectionSpecs, proxySelector);
- client.setProxy(proxyA);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ Address address = new Address(uriHost, uriPort, dns, socketFactory, null, null, null,
+ authenticator, proxyA, protocols, connectionSpecs, proxySelector);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(proxyAHost);
@@ -158,15 +129,14 @@ public final class RouteSelectorTest {
}
@Test public void explicitDirectProxy() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator,
- NO_PROXY, protocols, connectionSpecs, proxySelector);
- client.setProxy(NO_PROXY);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ Address address = new Address(uriHost, uriPort, dns, socketFactory, null, null, null,
+ authenticator, NO_PROXY, protocols, connectionSpecs, proxySelector);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(uriHost);
@@ -177,12 +147,12 @@ public final class RouteSelectorTest {
Address address = httpAddress();
proxySelector.proxies = null;
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -190,16 +160,16 @@ public final class RouteSelectorTest {
@Test public void proxySelectorReturnsNoProxies() throws Exception {
Address address = httpAddress();
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(uriHost);
- proxySelector.assertRequests(httpRequest.uri());
+ proxySelector.assertRequests(address.url().uri());
}
@Test public void proxySelectorReturnsMultipleProxies() throws Exception {
@@ -207,26 +177,26 @@ public final class RouteSelectorTest {
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
// First try the IP addresses of the first proxy, in sequence.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
dns.assertRequests(proxyAHost);
// Next try the IP address of the second proxy.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(254, 1);
- assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort);
+ dns.addresses(makeFakeAddresses(254, 1));
+ assertRoute(routeSelector.next(), address, proxyB, dns.address(0), proxyBPort);
dns.assertRequests(proxyBHost);
// Finally try the only IP address of the origin server.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(253, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(253, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -236,13 +206,13 @@ public final class RouteSelectorTest {
Address address = httpAddress();
proxySelector.proxies.add(NO_PROXY);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
// Only the origin server will be attempted.
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -254,16 +224,16 @@ public final class RouteSelectorTest {
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
proxySelector.proxies.add(proxyA);
- RouteSelector routeSelector = RouteSelector.get(address, httpRequest, client);
- proxySelector.assertRequests(httpRequest.uri());
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
+ proxySelector.assertRequests(address.url().uri());
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = null;
+ dns.unknownHost();
try {
routeSelector.next();
fail();
@@ -272,13 +242,13 @@ public final class RouteSelectorTest {
dns.assertRequests(proxyBHost);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(255, 1);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 1));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
- dns.inetAddresses = makeFakeAddresses(254, 1);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(254, 1));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -288,36 +258,35 @@ public final class RouteSelectorTest {
Address address = httpsAddress();
proxySelector.proxies.add(proxyA);
proxySelector.proxies.add(proxyB);
- RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
// Proxy A
- dns.inetAddresses = makeFakeAddresses(255, 2);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ dns.addresses(makeFakeAddresses(255, 2));
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(0), proxyAPort);
dns.assertRequests(proxyAHost);
- assertRoute(routeSelector.next(), address, proxyA, dns.inetAddresses[1], proxyAPort);
+ assertRoute(routeSelector.next(), address, proxyA, dns.address(1), proxyAPort);
// Proxy B
- dns.inetAddresses = makeFakeAddresses(254, 2);
- assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[0], proxyBPort);
+ dns.addresses(makeFakeAddresses(254, 2));
+ assertRoute(routeSelector.next(), address, proxyB, dns.address(0), proxyBPort);
dns.assertRequests(proxyBHost);
- assertRoute(routeSelector.next(), address, proxyB, dns.inetAddresses[1], proxyBPort);
+ assertRoute(routeSelector.next(), address, proxyB, dns.address(1), proxyBPort);
// Origin
- dns.inetAddresses = makeFakeAddresses(253, 2);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ dns.addresses(makeFakeAddresses(253, 2));
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(0), uriPort);
dns.assertRequests(uriHost);
- assertRoute(routeSelector.next(), address, NO_PROXY, dns.inetAddresses[1], uriPort);
+ assertRoute(routeSelector.next(), address, NO_PROXY, dns.address(1), uriPort);
assertFalse(routeSelector.hasNext());
}
@Test public void failedRoutesAreLast() throws Exception {
Address address = httpsAddress();
- client.setProxy(Proxy.NO_PROXY);
- RouteSelector routeSelector = RouteSelector.get(address, httpsRequest, client);
+ RouteSelector routeSelector = new RouteSelector(address, routeDatabase);
final int numberOfAddresses = 2;
- dns.inetAddresses = makeFakeAddresses(255, numberOfAddresses);
+ dns.addresses(makeFakeAddresses(255, numberOfAddresses));
// Extract the regular sequence of routes from selector.
List<Route> regularRoutes = new ArrayList<>();
@@ -330,7 +299,7 @@ public final class RouteSelectorTest {
// Add first regular route as failed.
routeDatabase.failed(regularRoutes.get(0));
// Reset selector
- routeSelector = RouteSelector.get(address, httpsRequest, client);
+ routeSelector = new RouteSelector(address, routeDatabase);
List<Route> routesWithFailedRoute = new ArrayList<>();
while (routeSelector.hasNext()) {
@@ -370,41 +339,25 @@ public final class RouteSelectorTest {
/** Returns an address that's without an SSL socket factory or hostname verifier. */
private Address httpAddress() {
- return new Address(uriHost, uriPort, socketFactory, null, null, null, authenticator, null,
+ return new Address(uriHost, uriPort, dns, socketFactory, null, null, null, authenticator, null,
protocols, connectionSpecs, proxySelector);
}
private Address httpsAddress() {
- return new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
+ return new Address(uriHost, uriPort, dns, socketFactory, sslSocketFactory,
hostnameVerifier, null, authenticator, null, protocols, connectionSpecs, proxySelector);
}
- private static InetAddress[] makeFakeAddresses(int prefix, int count) {
+ private static List<InetAddress> makeFakeAddresses(int prefix, int count) {
try {
- InetAddress[] result = new InetAddress[count];
+ List<InetAddress> result = new ArrayList<>();
for (int i = 0; i < count; i++) {
- result[i] =
- InetAddress.getByAddress(new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i });
+ result.add(InetAddress.getByAddress(
+ new byte[] { (byte) prefix, (byte) 0, (byte) 0, (byte) i }));
}
return result;
} catch (UnknownHostException e) {
throw new AssertionError();
}
}
-
- private static class FakeDns implements Network {
- List<String> requestedHosts = new ArrayList<>();
- InetAddress[] inetAddresses;
-
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- requestedHosts.add(host);
- if (inetAddresses == null) throw new UnknownHostException();
- return inetAddresses;
- }
-
- public void assertRequests(String... expectedHosts) {
- assertEquals(Arrays.asList(expectedHosts), requestedHosts);
- requestedHosts.clear();
- }
- }
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java
new file mode 100644
index 0000000..951d9e6
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificateChainCleanerTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.tls;
+
+import com.squareup.okhttp.internal.HeldCertificate;
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+public final class CertificateChainCleanerTest {
+ @Test public void normalizeSingleSelfSignedCertificate() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(root), council.clean(list(root)));
+ }
+
+ @Test public void normalizeUnknownSelfSignedCertificate() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex());
+
+ try {
+ council.clean(list(root));
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ }
+ }
+
+ @Test public void orderedChainOfCertificatesWithRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certB, certA, root), council.clean(list(certB, certA, root)));
+ }
+
+ @Test public void orderedChainOfCertificatesWithoutRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certB, certA, root), council.clean(list(certB, certA))); // Root is added!
+ }
+
+ @Test public void unorderedChainOfCertificatesWithRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+ HeldCertificate certC = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .issuedBy(certB)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certC, certB, certA, root), council.clean(list(certC, certA, root, certB)));
+ }
+
+ @Test public void unorderedChainOfCertificatesWithoutRoot() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+ HeldCertificate certC = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .issuedBy(certB)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certC, certB, certA, root), council.clean(list(certC, certA, certB)));
+ }
+
+ @Test public void unrelatedCertificatesAreOmitted() throws Exception {
+ HeldCertificate root = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(root)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(certA)
+ .build();
+ HeldCertificate certUnnecessary = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(root.certificate));
+ assertEquals(list(certB, certA, root),
+ council.clean(list(certB, certUnnecessary, certA, root)));
+ }
+
+ @Test public void chainGoesAllTheWayToSelfSignedRoot() throws Exception {
+ HeldCertificate selfSigned = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate trusted = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .issuedBy(selfSigned)
+ .build();
+ HeldCertificate certA = new HeldCertificate.Builder()
+ .serialNumber("3")
+ .issuedBy(trusted)
+ .build();
+ HeldCertificate certB = new HeldCertificate.Builder()
+ .serialNumber("4")
+ .issuedBy(certA)
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(selfSigned.certificate, trusted.certificate));
+ assertEquals(list(certB, certA, trusted, selfSigned),
+ council.clean(list(certB, certA)));
+ assertEquals(list(certB, certA, trusted, selfSigned),
+ council.clean(list(certB, certA, trusted)));
+ assertEquals(list(certB, certA, trusted, selfSigned),
+ council.clean(list(certB, certA, trusted, selfSigned)));
+ }
+
+ @Test public void trustedRootNotSelfSigned() throws Exception {
+ HeldCertificate unknownSigner = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .build();
+ HeldCertificate trusted = new HeldCertificate.Builder()
+ .issuedBy(unknownSigner)
+ .serialNumber("2")
+ .build();
+ HeldCertificate intermediateCa = new HeldCertificate.Builder()
+ .issuedBy(trusted)
+ .serialNumber("3")
+ .build();
+ HeldCertificate certificate = new HeldCertificate.Builder()
+ .issuedBy(intermediateCa)
+ .serialNumber("4")
+ .build();
+
+ CertificateChainCleaner council = new CertificateChainCleaner(
+ new RealTrustRootIndex(trusted.certificate));
+ assertEquals(list(certificate, intermediateCa, trusted),
+ council.clean(list(certificate, intermediateCa)));
+ assertEquals(list(certificate, intermediateCa, trusted),
+ council.clean(list(certificate, intermediateCa, trusted)));
+ }
+
+ @Test public void chainMaxLength() throws Exception {
+ List<HeldCertificate> heldCertificates = chainOfLength(10);
+ List<Certificate> certificates = new ArrayList<>();
+ for (HeldCertificate heldCertificate : heldCertificates) {
+ certificates.add(heldCertificate.certificate);
+ }
+
+ X509Certificate root = heldCertificates.get(heldCertificates.size() - 1).certificate;
+ CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex(root));
+ assertEquals(certificates, council.clean(certificates));
+ assertEquals(certificates, council.clean(certificates.subList(0, 9)));
+ }
+
+ @Test public void chainTooLong() throws Exception {
+ List<HeldCertificate> heldCertificates = chainOfLength(11);
+ List<Certificate> certificates = new ArrayList<>();
+ for (HeldCertificate heldCertificate : heldCertificates) {
+ certificates.add(heldCertificate.certificate);
+ }
+
+ X509Certificate root = heldCertificates.get(heldCertificates.size() - 1).certificate;
+ CertificateChainCleaner council = new CertificateChainCleaner(new RealTrustRootIndex(root));
+ try {
+ council.clean(certificates);
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ }
+ }
+
+ /** Returns a chain starting at the leaf certificate and progressing to the root. */
+ private List<HeldCertificate> chainOfLength(int length) throws GeneralSecurityException {
+ List<HeldCertificate> result = new ArrayList<>();
+ for (int i = 1; i <= length; i++) {
+ result.add(0, new HeldCertificate.Builder()
+ .issuedBy(!result.isEmpty() ? result.get(0) : null)
+ .serialNumber(Integer.toString(i))
+ .build());
+ }
+ return result;
+ }
+
+ private List<Certificate> list(HeldCertificate... heldCertificates) {
+ List<Certificate> result = new ArrayList<>();
+ for (HeldCertificate heldCertificate : heldCertificates) {
+ result.add(heldCertificate.certificate);
+ }
+ return result;
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java
new file mode 100644
index 0000000..5144dd2
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/tls/CertificatePinnerChainValidationTest.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.tls;
+
+import com.squareup.okhttp.Call;
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.internal.HeldCertificate;
+import com.squareup.okhttp.internal.SslContextBuilder;
+import com.squareup.okhttp.mockwebserver.MockResponse;
+import com.squareup.okhttp.mockwebserver.MockWebServer;
+import com.squareup.okhttp.mockwebserver.SocketPolicy;
+import com.squareup.okhttp.testing.RecordingHostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public final class CertificatePinnerChainValidationTest {
+ @Rule public final MockWebServer server = new MockWebServer();
+
+ /** The pinner should pull the root certificate from the trust manager. */
+ @Test public void pinRootNotPresentInChain() throws Exception {
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+ HeldCertificate intermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("2")
+ .commonName("intermediate_ca")
+ .build();
+ HeldCertificate certificate = new HeldCertificate.Builder()
+ .issuedBy(intermediateCa)
+ .serialNumber("3")
+ .commonName(server.getHostName())
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(rootCa.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(certificate, intermediateCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+
+ // The request should complete successfully.
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call1 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response1 = call1.execute();
+ assertEquals("abc", response1.body().string());
+
+ // Confirm that a second request also succeeds. This should detect caching problems.
+ server.enqueue(new MockResponse()
+ .setBody("def")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call2 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response2 = call2.execute();
+ assertEquals("def", response2.body().string());
+ }
+
+ /** The pinner should accept an intermediate from the server's chain. */
+ @Test public void pinIntermediatePresentInChain() throws Exception {
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+ HeldCertificate intermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("2")
+ .commonName("intermediate_ca")
+ .build();
+ HeldCertificate certificate = new HeldCertificate.Builder()
+ .issuedBy(intermediateCa)
+ .serialNumber("3")
+ .commonName(server.getHostName())
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(intermediateCa.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(certificate, intermediateCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+
+ // The request should complete successfully.
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call1 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response1 = call1.execute();
+ assertEquals("abc", response1.body().string());
+
+ // Confirm that a second request also succeeds. This should detect caching problems.
+ server.enqueue(new MockResponse()
+ .setBody("def")
+ .setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
+ Call call2 = client.newCall(new Request.Builder()
+ .url(server.url("/"))
+ .build());
+ Response response2 = call2.execute();
+ assertEquals("def", response2.body().string());
+ }
+
+ @Test public void unrelatedPinnedLeafCertificateInChain() throws Exception {
+ // Start with a trusted root CA certificate.
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+
+ // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
+ // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
+ // certificate.
+ HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("2")
+ .commonName("good_intermediate_ca")
+ .build();
+ HeldCertificate goodCertificate = new HeldCertificate.Builder()
+ .issuedBy(goodIntermediateCa)
+ .serialNumber("3")
+ .commonName(server.getHostName())
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(goodCertificate.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ // Add a bad intermediate CA and have that issue a rogue certificate for localhost. Prepare
+ // an SSL context for an attacking webserver. It includes both these rogue certificates plus the
+ // trusted good certificate above. The attack is that by including the good certificate in the
+ // chain, we may trick the certificate pinner into accepting the rouge certificate.
+ HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("4")
+ .commonName("bad_intermediate_ca")
+ .build();
+ HeldCertificate rogueCertificate = new HeldCertificate.Builder()
+ .serialNumber("5")
+ .issuedBy(compromisedIntermediateCa)
+ .commonName(server.getHostName())
+ .build();
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(rogueCertificate, compromisedIntermediateCa, goodCertificate, rootCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ // Make a request from client to server. It should succeed certificate checks (unfortunately the
+ // rogue CA is trusted) but it should fail certificate pinning.
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Call call = client.newCall(request);
+ try {
+ call.execute();
+ fail();
+ } catch (SSLPeerUnverifiedException expected) {
+ // Certificate pinning fails!
+ String message = expected.getMessage();
+ assertTrue(message, message.startsWith("Certificate pinning failure!"));
+ }
+ }
+
+ @Test public void unrelatedPinnedIntermediateCertificateInChain() throws Exception {
+ // Start with two root CA certificates, one is good and the other is compromised.
+ HeldCertificate rootCa = new HeldCertificate.Builder()
+ .serialNumber("1")
+ .ca(3)
+ .commonName("root")
+ .build();
+ HeldCertificate compromisedRootCa = new HeldCertificate.Builder()
+ .serialNumber("2")
+ .ca(3)
+ .commonName("compromised_root")
+ .build();
+
+ // Add a good intermediate CA, and have that issue a good certificate to localhost. Prepare an
+ // SSL context for an HTTP client under attack. It includes the trusted CA and a pinned
+ // certificate.
+ HeldCertificate goodIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(rootCa)
+ .ca(2)
+ .serialNumber("3")
+ .commonName("intermediate_ca")
+ .build();
+ CertificatePinner certificatePinner = new CertificatePinner.Builder()
+ .add(server.getHostName(), CertificatePinner.pin(goodIntermediateCa.certificate))
+ .build();
+ SSLContext clientContext = new SslContextBuilder()
+ .addTrustedCertificate(rootCa.certificate)
+ .addTrustedCertificate(compromisedRootCa.certificate)
+ .build();
+ OkHttpClient client = new OkHttpClient()
+ .setSslSocketFactory(clientContext.getSocketFactory())
+ .setHostnameVerifier(new RecordingHostnameVerifier())
+ .setCertificatePinner(certificatePinner);
+
+ // The attacker compromises the root CA, issues an intermediate with the same common name
+ // "intermediate_ca" as the good CA. This signs a rogue certificate for localhost. The server
+ // serves the good CAs certificate in the chain, which means the certificate pinner sees a
+ // different set of certificates than the SSL verifier.
+ HeldCertificate compromisedIntermediateCa = new HeldCertificate.Builder()
+ .issuedBy(compromisedRootCa)
+ .ca(2)
+ .serialNumber("4")
+ .commonName("intermediate_ca")
+ .build();
+ HeldCertificate rogueCertificate = new HeldCertificate.Builder()
+ .serialNumber("5")
+ .issuedBy(compromisedIntermediateCa)
+ .commonName(server.getHostName())
+ .build();
+ SSLContext serverSslContext = new SslContextBuilder()
+ .certificateChain(
+ rogueCertificate, goodIntermediateCa, compromisedIntermediateCa, compromisedRootCa)
+ .build();
+ server.useHttps(serverSslContext.getSocketFactory(), false);
+ server.enqueue(new MockResponse()
+ .setBody("abc")
+ .addHeader("Content-Type: text/plain"));
+
+ // Make a request from client to server. It should succeed certificate checks (unfortunately the
+ // rogue CA is trusted) but it should fail certificate pinning.
+ Request request = new Request.Builder()
+ .url(server.url("/"))
+ .build();
+ Call call = client.newCall(request);
+ try {
+ call.execute();
+ fail();
+ } catch (SSLHandshakeException expected) {
+ // On Android, the handshake fails before the certificate pinner runs.
+ String message = expected.getMessage();
+ assertTrue(message, message.contains("Could not validate certificate"));
+ } catch (SSLPeerUnverifiedException expected) {
+ // On OpenJDK, the handshake succeeds but the certificate pinner fails.
+ String message = expected.getMessage();
+ assertTrue(message, message.startsWith("Certificate pinning failure!"));
+ }
+ }
+}
diff --git a/okhttp-urlconnection/pom.xml b/okhttp-urlconnection/pom.xml
index be60560..c11c67f 100644
--- a/okhttp-urlconnection/pom.xml
+++ b/okhttp-urlconnection/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-urlconnection</artifactId>
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java
index 30d8b49..4b34559 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/OkUrlFactory.java
@@ -15,7 +15,6 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.URLFilter;
import com.squareup.okhttp.internal.huc.HttpURLConnectionImpl;
import com.squareup.okhttp.internal.huc.HttpsURLConnectionImpl;
@@ -28,7 +27,6 @@ import java.net.URLStreamHandlerFactory;
public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable {
private final OkHttpClient client;
- private URLFilter urlFilter;
public OkUrlFactory(OkHttpClient client) {
this.client = client;
@@ -38,10 +36,6 @@ public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable {
return client;
}
- void setUrlFilter(URLFilter filter) {
- urlFilter = filter;
- }
-
/**
* Returns a copy of this stream handler factory that includes a shallow copy
* of the internal {@linkplain OkHttpClient HTTP client}.
@@ -59,8 +53,8 @@ public final class OkUrlFactory implements URLStreamHandlerFactory, Cloneable {
OkHttpClient copy = client.copyWithDefaults();
copy.setProxy(proxy);
- if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy, urlFilter);
- if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy, urlFilter);
+ if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy);
+ if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy);
throw new IllegalArgumentException("Unexpected protocol: " + protocol);
}
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/URLFilter.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/URLFilter.java
deleted file mode 100644
index 52745e9..0000000
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/URLFilter.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2016 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-import java.io.IOException;
-import java.net.URL;
-
-/**
- * Request filter based on the request's URL.
- *
- * @deprecated use {@link okhttp3.Interceptor} for non-HttpURLConnection filtering.
- */
-public interface URLFilter {
- /**
- * Check whether request to the provided URL is permitted to be issued.
- *
- * @throws IOException if the request to the provided URL is not permitted.
- */
- void checkURLPermitted(URL url) throws IOException;
-}
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
index 454f6c3..b7b8c72 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpURLConnectionImpl.java
@@ -29,7 +29,6 @@ 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.URLFilter;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.Version;
import com.squareup.okhttp.internal.http.HttpDate;
@@ -40,6 +39,7 @@ import com.squareup.okhttp.internal.http.RequestException;
import com.squareup.okhttp.internal.http.RetryableSink;
import com.squareup.okhttp.internal.http.RouteException;
import com.squareup.okhttp.internal.http.StatusLine;
+import com.squareup.okhttp.internal.http.StreamAllocation;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -73,7 +73,7 @@ import okio.Sink;
*
* <h3>What does 'connected' mean?</h3>
* This class inherits a {@code connected} field from the superclass. That field
- * is <strong>not</strong> used to indicate not whether this URLConnection is
+ * is <strong>not</strong> used to indicate whether this URLConnection is
* currently connected. Instead, it indicates whether a connection has ever been
* attempted. Once a connection has been attempted, certain properties (request
* header fields, request method, etc.) are immutable.
@@ -107,18 +107,11 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
*/
Handshake handshake;
- private URLFilter urlFilter;
-
public HttpURLConnectionImpl(URL url, OkHttpClient client) {
super(url);
this.client = client;
}
- public HttpURLConnectionImpl(URL url, OkHttpClient client, URLFilter urlFilter) {
- this(url, client);
- this.urlFilter = urlFilter;
- }
-
@Override public final void connect() throws IOException {
initHttpEngine();
boolean success;
@@ -131,7 +124,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
// Calling disconnect() before a connection exists should have no effect.
if (httpEngine == null) return;
- httpEngine.disconnect();
+ httpEngine.cancel();
// This doesn't close the stream because doing so would require all stream
// access to be synchronized. It's expected that the thread using the
@@ -161,9 +154,9 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
if (responseHeaders == null) {
Response response = getResponse().getResponse();
Headers headers = response.headers();
-
responseHeaders = headers.newBuilder()
- .add(Platform.get().getPrefix() + "-Response-Source", responseSourceHeader(response))
+ .add(OkHeaders.SELECTED_PROTOCOL, response.protocol().toString())
+ .add(OkHeaders.RESPONSE_SOURCE, responseSourceHeader(response))
.build();
}
return responseHeaders;
@@ -335,8 +328,9 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
}
}
- private HttpEngine newHttpEngine(String method, Connection connection, RetryableSink requestBody,
- Response priorResponse) throws MalformedURLException, UnknownHostException {
+ private HttpEngine newHttpEngine(String method, StreamAllocation streamAllocation,
+ RetryableSink requestBody, Response priorResponse)
+ throws MalformedURLException, UnknownHostException {
// OkHttp's Call API requires a placeholder body; the real body will be streamed separately.
RequestBody placeholderBody = HttpMethod.requiresRequestBody(method)
? EMPTY_REQUEST_BODY
@@ -380,7 +374,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
engineClient = client.clone().setCache(null);
}
- return new HttpEngine(engineClient, request, bufferRequestBody, true, false, connection, null,
+ return new HttpEngine(engineClient, request, bufferRequestBody, true, false, streamAllocation,
requestBody, priorResponse);
}
@@ -410,7 +404,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
Request followUp = httpEngine.followUpRequest();
if (followUp == null) {
- httpEngine.releaseConnection();
+ httpEngine.releaseStreamAllocation();
return httpEngine;
}
@@ -434,12 +428,13 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
throw new HttpRetryException("Cannot retry streamed HTTP body", responseCode);
}
+ StreamAllocation streamAllocation = httpEngine.close();
if (!httpEngine.sameConnection(followUp.httpUrl())) {
- httpEngine.releaseConnection();
+ streamAllocation.release();
+ streamAllocation = null;
}
- Connection connection = httpEngine.close();
- httpEngine = newHttpEngine(followUp.method(), connection, (RetryableSink) requestBody,
+ httpEngine = newHttpEngine(followUp.method(), streamAllocation, (RetryableSink) requestBody,
response);
}
}
@@ -450,18 +445,21 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
* retried. Throws an exception if the request failed permanently.
*/
private boolean execute(boolean readResponse) throws IOException {
- if (urlFilter != null) {
- urlFilter.checkURLPermitted(httpEngine.getRequest().url());
- }
+ boolean releaseConnection = true;
try {
httpEngine.sendRequest();
- route = httpEngine.getRoute();
- handshake = httpEngine.getConnection() != null
- ? httpEngine.getConnection().getHandshake()
- : null;
+ Connection connection = httpEngine.getConnection();
+ if (connection != null) {
+ route = connection.getRoute();
+ handshake = connection.getHandshake();
+ } else {
+ route = null;
+ handshake = null;
+ }
if (readResponse) {
httpEngine.readResponse();
}
+ releaseConnection = false;
return true;
} catch (RequestException e) {
@@ -473,6 +471,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = httpEngine.recover(e);
if (retryEngine != null) {
+ releaseConnection = false;
httpEngine = retryEngine;
return false;
}
@@ -485,6 +484,7 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = httpEngine.recover(e);
if (retryEngine != null) {
+ releaseConnection = false;
httpEngine = retryEngine;
return false;
}
@@ -492,6 +492,12 @@ public class HttpURLConnectionImpl extends HttpURLConnection {
// Give up; recovery is not possible.
httpEngineFailure = e;
throw e;
+ } finally {
+ // We're throwing an unchecked exception. Release any resources.
+ if (releaseConnection) {
+ StreamAllocation streamAllocation = httpEngine.close();
+ streamAllocation.release();
+ }
}
}
diff --git a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
index 140b4d2..2aba087 100644
--- a/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
+++ b/okhttp-urlconnection/src/main/java/com/squareup/okhttp/internal/huc/HttpsURLConnectionImpl.java
@@ -18,7 +18,6 @@ package com.squareup.okhttp.internal.huc;
import com.squareup.okhttp.Handshake;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.internal.URLFilter;
import java.net.URL;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
@@ -30,10 +29,6 @@ public final class HttpsURLConnectionImpl extends DelegatingHttpsURLConnection {
this(new HttpURLConnectionImpl(url, client));
}
- public HttpsURLConnectionImpl(URL url, OkHttpClient client, URLFilter filter) {
- this(new HttpURLConnectionImpl(url, client, filter));
- }
-
public HttpsURLConnectionImpl(HttpURLConnectionImpl delegate) {
super(delegate);
this.delegate = delegate;
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
index 983dd57..0b16929 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/OkUrlFactoryTest.java
@@ -1,21 +1,20 @@
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.URLFilter;
-import com.squareup.okhttp.internal.io.FileSystem;
+import com.squareup.okhttp.internal.http.OkHeaders;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
-import java.net.URL;
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 okio.BufferedSource;
+import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
@@ -28,16 +27,22 @@ import static org.junit.Assert.fail;
public class OkUrlFactoryTest {
@Rule public MockWebServer server = new MockWebServer();
+ @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
- private FileSystem fileSystem = new InMemoryFileSystem();
private OkUrlFactory factory;
+ private Cache cache;
@Before public void setUp() throws IOException {
OkHttpClient client = new OkHttpClient();
- client.setCache(new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem));
+ cache = new Cache(new File("/cache/"), 10 * 1024 * 1024, fileSystem);
+ client.setCache(cache);
factory = new OkUrlFactory(client);
}
+ @After public void tearDown() throws IOException {
+ cache.delete();
+ }
+
/**
* Response code 407 should only come from proxy servers. Android's client
* throws if it is sent by an origin server.
@@ -66,6 +71,7 @@ public class OkUrlFactoryTest {
HttpURLConnection connection = factory.open(server.getUrl("/"));
assertResponseHeader(connection, "NETWORK 404");
+ connection.getErrorStream().close();
}
@Test public void conditionalCacheHitResponseSourceHeaders() throws Exception {
@@ -141,38 +147,15 @@ public class OkUrlFactoryTest {
assertResponseCode(connection, 302);
}
- @Test
- public void testURLFilter() throws Exception {
- server.enqueue(new MockResponse()
- .setBody("B"));
- final URL blockedURL = server.url("/a").url();
- factory.setUrlFilter(new URLFilter() {
- @Override
- public void checkURLPermitted(URL url) throws IOException {
- if (blockedURL.equals(url)) {
- throw new IOException("Blocked");
- }
- }
- });
- try {
- HttpURLConnection connection = factory.open(server.url("/a").url());
- connection.getInputStream();
- fail("Connection was successful");
- } catch (IOException e) {
- assertEquals("Blocked", e.getMessage());
- }
- HttpURLConnection connection = factory.open(server.url("/b").url());
- assertResponseBody(connection, "B");
- }
-
private void assertResponseBody(HttpURLConnection connection, String expected) throws Exception {
- String actual = buffer(source(connection.getInputStream())).readString(US_ASCII);
+ BufferedSource source = buffer(source(connection.getInputStream()));
+ String actual = source.readString(US_ASCII);
+ source.close();
assertEquals(expected, actual);
}
private void assertResponseHeader(HttpURLConnection connection, String expected) {
- final String headerFieldPrefix = Platform.get().getPrefix();
- assertEquals(expected, connection.getHeaderField(headerFieldPrefix + "-Response-Source"));
+ assertEquals(expected, connection.getHeaderField(OkHeaders.RESPONSE_SOURCE));
}
private void assertResponseCode(HttpURLConnection connection, int expected) throws IOException {
diff --git a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
index 0af815b..66bf7c2 100644
--- a/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
+++ b/okhttp-urlconnection/src/test/java/com/squareup/okhttp/UrlConnectionCacheTest.java
@@ -19,7 +19,6 @@ package com.squareup.okhttp;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.io.FileSystem;
import com.squareup.okhttp.internal.io.InMemoryFileSystem;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -81,9 +80,9 @@ public final class UrlConnectionCacheTest {
@Rule public MockWebServer server = new MockWebServer();
@Rule public MockWebServer server2 = new MockWebServer();
+ @Rule public InMemoryFileSystem fileSystem = new InMemoryFileSystem();
private final SSLContext sslContext = SslContextBuilder.localhost();
- private final FileSystem fileSystem = new InMemoryFileSystem();
private final OkUrlFactory client = new OkUrlFactory(new OkHttpClient());
private Cache cache;
private final CookieManager cookieManager = new CookieManager();
@@ -98,6 +97,7 @@ public final class UrlConnectionCacheTest {
@After public void tearDown() throws Exception {
ResponseCache.setDefault(null);
CookieHandler.setDefault(null);
+ cache.delete();
}
@Test public void responseCacheAccessWithOkHttpMember() throws IOException {
@@ -855,7 +855,7 @@ public final class UrlConnectionCacheTest {
assertEquals("A", readAscii(client.open(server.getUrl("/"))));
assertEquals("A", readAscii(client.open(server.getUrl("/"))));
- assertEquals(1, client.client().getConnectionPool().getConnectionCount());
+ assertEquals(1, client.client().getConnectionPool().getIdleConnectionCount());
}
@Test public void expiresDateBeforeModifiedDate() throws Exception {
@@ -1586,6 +1586,7 @@ public final class UrlConnectionCacheTest {
HttpURLConnection connection = client.open(server.getUrl("/"));
assertEquals("A", connection.getHeaderField(""));
+ assertEquals("body", readAscii(connection));
}
/**
diff --git a/okhttp-ws-tests/fuzzingserver-config.json b/okhttp-ws-tests/fuzzingserver-config.json
new file mode 100644
index 0000000..99e06ab
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-config.json
@@ -0,0 +1,153 @@
+{
+ "url": "ws://127.0.0.1:9001",
+ "outdir": "./target/fuzzingserver-report",
+ "cases": ["*"],
+ "exclude-cases": [
+ "6.1.1",
+ "6.1.2",
+ "6.1.3",
+ "6.2.1",
+ "6.2.2",
+ "6.2.3",
+ "6.2.4",
+ "6.3.1",
+ "6.3.2",
+ "6.4.1",
+ "6.4.2",
+ "6.4.3",
+ "6.4.4",
+ "6.5.1",
+ "6.5.2",
+ "6.5.3",
+ "6.5.4",
+ "6.5.5",
+ "6.6.1",
+ "6.6.2",
+ "6.6.3",
+ "6.6.4",
+ "6.6.5",
+ "6.6.6",
+ "6.6.7",
+ "6.6.8",
+ "6.6.9",
+ "6.6.10",
+ "6.6.11",
+ "6.7.1",
+ "6.7.2",
+ "6.7.3",
+ "6.7.4",
+ "6.8.1",
+ "6.8.2",
+ "6.9.1",
+ "6.9.2",
+ "6.9.3",
+ "6.9.4",
+ "6.10.1",
+ "6.10.2",
+ "6.10.3",
+ "6.11.1",
+ "6.11.2",
+ "6.11.3",
+ "6.11.4",
+ "6.11.5",
+ "6.12.1",
+ "6.12.2",
+ "6.12.3",
+ "6.12.4",
+ "6.12.5",
+ "6.12.6",
+ "6.12.7",
+ "6.12.8",
+ "6.13.1",
+ "6.13.2",
+ "6.13.3",
+ "6.13.4",
+ "6.13.5",
+ "6.14.1",
+ "6.14.2",
+ "6.14.3",
+ "6.14.4",
+ "6.14.5",
+ "6.14.6",
+ "6.14.7",
+ "6.14.8",
+ "6.14.9",
+ "6.14.10",
+ "6.15.1",
+ "6.16.1",
+ "6.16.2",
+ "6.16.3",
+ "6.17.1",
+ "6.17.2",
+ "6.17.3",
+ "6.17.4",
+ "6.17.5",
+ "6.18.1",
+ "6.18.2",
+ "6.18.3",
+ "6.18.4",
+ "6.18.5",
+ "6.19.1",
+ "6.19.2",
+ "6.19.3",
+ "6.19.4",
+ "6.19.5",
+ "6.20.1",
+ "6.20.2",
+ "6.20.3",
+ "6.20.4",
+ "6.20.5",
+ "6.20.6",
+ "6.20.7",
+ "6.21.1",
+ "6.21.2",
+ "6.21.3",
+ "6.21.4",
+ "6.21.5",
+ "6.21.6",
+ "6.21.7",
+ "6.21.8",
+ "6.22.1",
+ "6.22.2",
+ "6.22.3",
+ "6.22.4",
+ "6.22.5",
+ "6.22.6",
+ "6.22.7",
+ "6.22.8",
+ "6.22.9",
+ "6.22.10",
+ "6.22.11",
+ "6.22.12",
+ "6.22.13",
+ "6.22.14",
+ "6.22.15",
+ "6.22.16",
+ "6.22.17",
+ "6.22.18",
+ "6.22.19",
+ "6.22.20",
+ "6.22.21",
+ "6.22.22",
+ "6.22.23",
+ "6.22.24",
+ "6.22.25",
+ "6.22.26",
+ "6.22.27",
+ "6.22.28",
+ "6.22.29",
+ "6.22.30",
+ "6.22.31",
+ "6.22.32",
+ "6.22.33",
+ "6.22.34",
+ "6.23.1",
+ "6.23.2",
+ "6.23.3",
+ "6.23.4",
+ "6.23.5",
+ "6.23.6",
+ "6.23.7"
+ ],
+ "exclude-agent-cases": {}
+}
diff --git a/okhttp-ws-tests/fuzzingserver-expected.txt b/okhttp-ws-tests/fuzzingserver-expected.txt
new file mode 100644
index 0000000..f4a3305
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-expected.txt
@@ -0,0 +1,376 @@
+"1.1.1 OK"
+"1.1.2 OK"
+"1.1.3 OK"
+"1.1.4 OK"
+"1.1.5 OK"
+"1.1.6 OK"
+"1.1.7 OK"
+"1.1.8 OK"
+"1.2.1 OK"
+"1.2.2 OK"
+"1.2.3 OK"
+"1.2.4 OK"
+"1.2.5 OK"
+"1.2.6 OK"
+"1.2.7 OK"
+"1.2.8 OK"
+"10.1.1 OK"
+"12.1.1 UNIMPLEMENTED"
+"12.1.10 UNIMPLEMENTED"
+"12.1.11 UNIMPLEMENTED"
+"12.1.12 UNIMPLEMENTED"
+"12.1.13 UNIMPLEMENTED"
+"12.1.14 UNIMPLEMENTED"
+"12.1.15 UNIMPLEMENTED"
+"12.1.16 UNIMPLEMENTED"
+"12.1.17 UNIMPLEMENTED"
+"12.1.18 UNIMPLEMENTED"
+"12.1.2 UNIMPLEMENTED"
+"12.1.3 UNIMPLEMENTED"
+"12.1.4 UNIMPLEMENTED"
+"12.1.5 UNIMPLEMENTED"
+"12.1.6 UNIMPLEMENTED"
+"12.1.7 UNIMPLEMENTED"
+"12.1.8 UNIMPLEMENTED"
+"12.1.9 UNIMPLEMENTED"
+"12.2.1 UNIMPLEMENTED"
+"12.2.10 UNIMPLEMENTED"
+"12.2.11 UNIMPLEMENTED"
+"12.2.12 UNIMPLEMENTED"
+"12.2.13 UNIMPLEMENTED"
+"12.2.14 UNIMPLEMENTED"
+"12.2.15 UNIMPLEMENTED"
+"12.2.16 UNIMPLEMENTED"
+"12.2.17 UNIMPLEMENTED"
+"12.2.18 UNIMPLEMENTED"
+"12.2.2 UNIMPLEMENTED"
+"12.2.3 UNIMPLEMENTED"
+"12.2.4 UNIMPLEMENTED"
+"12.2.5 UNIMPLEMENTED"
+"12.2.6 UNIMPLEMENTED"
+"12.2.7 UNIMPLEMENTED"
+"12.2.8 UNIMPLEMENTED"
+"12.2.9 UNIMPLEMENTED"
+"12.3.1 UNIMPLEMENTED"
+"12.3.10 UNIMPLEMENTED"
+"12.3.11 UNIMPLEMENTED"
+"12.3.12 UNIMPLEMENTED"
+"12.3.13 UNIMPLEMENTED"
+"12.3.14 UNIMPLEMENTED"
+"12.3.15 UNIMPLEMENTED"
+"12.3.16 UNIMPLEMENTED"
+"12.3.17 UNIMPLEMENTED"
+"12.3.18 UNIMPLEMENTED"
+"12.3.2 UNIMPLEMENTED"
+"12.3.3 UNIMPLEMENTED"
+"12.3.4 UNIMPLEMENTED"
+"12.3.5 UNIMPLEMENTED"
+"12.3.6 UNIMPLEMENTED"
+"12.3.7 UNIMPLEMENTED"
+"12.3.8 UNIMPLEMENTED"
+"12.3.9 UNIMPLEMENTED"
+"12.4.1 UNIMPLEMENTED"
+"12.4.10 UNIMPLEMENTED"
+"12.4.11 UNIMPLEMENTED"
+"12.4.12 UNIMPLEMENTED"
+"12.4.13 UNIMPLEMENTED"
+"12.4.14 UNIMPLEMENTED"
+"12.4.15 UNIMPLEMENTED"
+"12.4.16 UNIMPLEMENTED"
+"12.4.17 UNIMPLEMENTED"
+"12.4.18 UNIMPLEMENTED"
+"12.4.2 UNIMPLEMENTED"
+"12.4.3 UNIMPLEMENTED"
+"12.4.4 UNIMPLEMENTED"
+"12.4.5 UNIMPLEMENTED"
+"12.4.6 UNIMPLEMENTED"
+"12.4.7 UNIMPLEMENTED"
+"12.4.8 UNIMPLEMENTED"
+"12.4.9 UNIMPLEMENTED"
+"12.5.1 UNIMPLEMENTED"
+"12.5.10 UNIMPLEMENTED"
+"12.5.11 UNIMPLEMENTED"
+"12.5.12 UNIMPLEMENTED"
+"12.5.13 UNIMPLEMENTED"
+"12.5.14 UNIMPLEMENTED"
+"12.5.15 UNIMPLEMENTED"
+"12.5.16 UNIMPLEMENTED"
+"12.5.17 UNIMPLEMENTED"
+"12.5.18 UNIMPLEMENTED"
+"12.5.2 UNIMPLEMENTED"
+"12.5.3 UNIMPLEMENTED"
+"12.5.4 UNIMPLEMENTED"
+"12.5.5 UNIMPLEMENTED"
+"12.5.6 UNIMPLEMENTED"
+"12.5.7 UNIMPLEMENTED"
+"12.5.8 UNIMPLEMENTED"
+"12.5.9 UNIMPLEMENTED"
+"13.1.1 UNIMPLEMENTED"
+"13.1.10 UNIMPLEMENTED"
+"13.1.11 UNIMPLEMENTED"
+"13.1.12 UNIMPLEMENTED"
+"13.1.13 UNIMPLEMENTED"
+"13.1.14 UNIMPLEMENTED"
+"13.1.15 UNIMPLEMENTED"
+"13.1.16 UNIMPLEMENTED"
+"13.1.17 UNIMPLEMENTED"
+"13.1.18 UNIMPLEMENTED"
+"13.1.2 UNIMPLEMENTED"
+"13.1.3 UNIMPLEMENTED"
+"13.1.4 UNIMPLEMENTED"
+"13.1.5 UNIMPLEMENTED"
+"13.1.6 UNIMPLEMENTED"
+"13.1.7 UNIMPLEMENTED"
+"13.1.8 UNIMPLEMENTED"
+"13.1.9 UNIMPLEMENTED"
+"13.2.1 UNIMPLEMENTED"
+"13.2.10 UNIMPLEMENTED"
+"13.2.11 UNIMPLEMENTED"
+"13.2.12 UNIMPLEMENTED"
+"13.2.13 UNIMPLEMENTED"
+"13.2.14 UNIMPLEMENTED"
+"13.2.15 UNIMPLEMENTED"
+"13.2.16 UNIMPLEMENTED"
+"13.2.17 UNIMPLEMENTED"
+"13.2.18 UNIMPLEMENTED"
+"13.2.2 UNIMPLEMENTED"
+"13.2.3 UNIMPLEMENTED"
+"13.2.4 UNIMPLEMENTED"
+"13.2.5 UNIMPLEMENTED"
+"13.2.6 UNIMPLEMENTED"
+"13.2.7 UNIMPLEMENTED"
+"13.2.8 UNIMPLEMENTED"
+"13.2.9 UNIMPLEMENTED"
+"13.3.1 UNIMPLEMENTED"
+"13.3.10 UNIMPLEMENTED"
+"13.3.11 UNIMPLEMENTED"
+"13.3.12 UNIMPLEMENTED"
+"13.3.13 UNIMPLEMENTED"
+"13.3.14 UNIMPLEMENTED"
+"13.3.15 UNIMPLEMENTED"
+"13.3.16 UNIMPLEMENTED"
+"13.3.17 UNIMPLEMENTED"
+"13.3.18 UNIMPLEMENTED"
+"13.3.2 UNIMPLEMENTED"
+"13.3.3 UNIMPLEMENTED"
+"13.3.4 UNIMPLEMENTED"
+"13.3.5 UNIMPLEMENTED"
+"13.3.6 UNIMPLEMENTED"
+"13.3.7 UNIMPLEMENTED"
+"13.3.8 UNIMPLEMENTED"
+"13.3.9 UNIMPLEMENTED"
+"13.4.1 UNIMPLEMENTED"
+"13.4.10 UNIMPLEMENTED"
+"13.4.11 UNIMPLEMENTED"
+"13.4.12 UNIMPLEMENTED"
+"13.4.13 UNIMPLEMENTED"
+"13.4.14 UNIMPLEMENTED"
+"13.4.15 UNIMPLEMENTED"
+"13.4.16 UNIMPLEMENTED"
+"13.4.17 UNIMPLEMENTED"
+"13.4.18 UNIMPLEMENTED"
+"13.4.2 UNIMPLEMENTED"
+"13.4.3 UNIMPLEMENTED"
+"13.4.4 UNIMPLEMENTED"
+"13.4.5 UNIMPLEMENTED"
+"13.4.6 UNIMPLEMENTED"
+"13.4.7 UNIMPLEMENTED"
+"13.4.8 UNIMPLEMENTED"
+"13.4.9 UNIMPLEMENTED"
+"13.5.1 UNIMPLEMENTED"
+"13.5.10 UNIMPLEMENTED"
+"13.5.11 UNIMPLEMENTED"
+"13.5.12 UNIMPLEMENTED"
+"13.5.13 UNIMPLEMENTED"
+"13.5.14 UNIMPLEMENTED"
+"13.5.15 UNIMPLEMENTED"
+"13.5.16 UNIMPLEMENTED"
+"13.5.17 UNIMPLEMENTED"
+"13.5.18 UNIMPLEMENTED"
+"13.5.2 UNIMPLEMENTED"
+"13.5.3 UNIMPLEMENTED"
+"13.5.4 UNIMPLEMENTED"
+"13.5.5 UNIMPLEMENTED"
+"13.5.6 UNIMPLEMENTED"
+"13.5.7 UNIMPLEMENTED"
+"13.5.8 UNIMPLEMENTED"
+"13.5.9 UNIMPLEMENTED"
+"13.6.1 UNIMPLEMENTED"
+"13.6.10 UNIMPLEMENTED"
+"13.6.11 UNIMPLEMENTED"
+"13.6.12 UNIMPLEMENTED"
+"13.6.13 UNIMPLEMENTED"
+"13.6.14 UNIMPLEMENTED"
+"13.6.15 UNIMPLEMENTED"
+"13.6.16 UNIMPLEMENTED"
+"13.6.17 UNIMPLEMENTED"
+"13.6.18 UNIMPLEMENTED"
+"13.6.2 UNIMPLEMENTED"
+"13.6.3 UNIMPLEMENTED"
+"13.6.4 UNIMPLEMENTED"
+"13.6.5 UNIMPLEMENTED"
+"13.6.6 UNIMPLEMENTED"
+"13.6.7 UNIMPLEMENTED"
+"13.6.8 UNIMPLEMENTED"
+"13.6.9 UNIMPLEMENTED"
+"13.7.1 UNIMPLEMENTED"
+"13.7.10 UNIMPLEMENTED"
+"13.7.11 UNIMPLEMENTED"
+"13.7.12 UNIMPLEMENTED"
+"13.7.13 UNIMPLEMENTED"
+"13.7.14 UNIMPLEMENTED"
+"13.7.15 UNIMPLEMENTED"
+"13.7.16 UNIMPLEMENTED"
+"13.7.17 UNIMPLEMENTED"
+"13.7.18 UNIMPLEMENTED"
+"13.7.2 UNIMPLEMENTED"
+"13.7.3 UNIMPLEMENTED"
+"13.7.4 UNIMPLEMENTED"
+"13.7.5 UNIMPLEMENTED"
+"13.7.6 UNIMPLEMENTED"
+"13.7.7 UNIMPLEMENTED"
+"13.7.8 UNIMPLEMENTED"
+"13.7.9 UNIMPLEMENTED"
+"2.1 OK"
+"2.10 OK"
+"2.11 OK"
+"2.2 OK"
+"2.3 OK"
+"2.4 OK"
+"2.5 OK"
+"2.6 OK"
+"2.7 OK"
+"2.8 OK"
+"2.9 OK"
+"3.1 OK"
+"3.2 NON-STRICT"
+"3.3 NON-STRICT"
+"3.4 NON-STRICT"
+"3.5 OK"
+"3.6 OK"
+"3.7 OK"
+"4.1.1 OK"
+"4.1.2 OK"
+"4.1.3 NON-STRICT"
+"4.1.4 NON-STRICT"
+"4.1.5 OK"
+"4.2.1 OK"
+"4.2.2 OK"
+"4.2.3 NON-STRICT"
+"4.2.4 NON-STRICT"
+"4.2.5 OK"
+"5.1 OK"
+"5.10 OK"
+"5.11 OK"
+"5.12 OK"
+"5.13 OK"
+"5.14 OK"
+"5.15 NON-STRICT"
+"5.16 OK"
+"5.17 OK"
+"5.18 OK"
+"5.19 OK"
+"5.2 OK"
+"5.20 OK"
+"5.3 OK"
+"5.4 OK"
+"5.5 OK"
+"5.6 OK"
+"5.7 OK"
+"5.8 OK"
+"5.9 OK"
+"7.1.1 OK"
+"7.1.2 OK"
+"7.1.3 OK"
+"7.1.4 OK"
+"7.1.5 FAILED"
+"7.1.6 INFORMATIONAL"
+"7.13.1 INFORMATIONAL"
+"7.13.2 INFORMATIONAL"
+"7.3.1 OK"
+"7.3.2 OK"
+"7.3.3 OK"
+"7.3.4 OK"
+"7.3.5 OK"
+"7.3.6 OK"
+"7.5.1 FAILED"
+"7.7.1 OK"
+"7.7.10 OK"
+"7.7.11 OK"
+"7.7.12 OK"
+"7.7.13 OK"
+"7.7.2 OK"
+"7.7.3 OK"
+"7.7.4 OK"
+"7.7.5 OK"
+"7.7.6 OK"
+"7.7.7 OK"
+"7.7.8 OK"
+"7.7.9 OK"
+"7.9.1 OK"
+"7.9.10 OK"
+"7.9.11 OK"
+"7.9.12 OK"
+"7.9.13 OK"
+"7.9.2 OK"
+"7.9.3 OK"
+"7.9.4 OK"
+"7.9.5 OK"
+"7.9.6 OK"
+"7.9.7 OK"
+"7.9.8 OK"
+"7.9.9 OK"
+"9.1.1 OK"
+"9.1.2 OK"
+"9.1.3 OK"
+"9.1.4 OK"
+"9.1.5 OK"
+"9.1.6 OK"
+"9.2.1 OK"
+"9.2.2 OK"
+"9.2.3 OK"
+"9.2.4 OK"
+"9.2.5 OK"
+"9.2.6 OK"
+"9.3.1 OK"
+"9.3.2 OK"
+"9.3.3 OK"
+"9.3.4 OK"
+"9.3.5 OK"
+"9.3.6 OK"
+"9.3.7 OK"
+"9.3.8 OK"
+"9.3.9 OK"
+"9.4.1 OK"
+"9.4.2 OK"
+"9.4.3 OK"
+"9.4.4 OK"
+"9.4.5 OK"
+"9.4.6 OK"
+"9.4.7 OK"
+"9.4.8 OK"
+"9.4.9 OK"
+"9.5.1 OK"
+"9.5.2 OK"
+"9.5.3 OK"
+"9.5.4 OK"
+"9.5.5 OK"
+"9.5.6 OK"
+"9.6.1 OK"
+"9.6.2 OK"
+"9.6.3 OK"
+"9.6.4 OK"
+"9.6.5 OK"
+"9.6.6 OK"
+"9.7.1 OK"
+"9.7.2 OK"
+"9.7.3 OK"
+"9.7.4 OK"
+"9.7.5 OK"
+"9.7.6 OK"
+"9.8.1 OK"
+"9.8.2 OK"
+"9.8.3 OK"
+"9.8.4 OK"
+"9.8.5 OK"
+"9.8.6 OK"
diff --git a/okhttp-ws-tests/fuzzingserver-test.sh b/okhttp-ws-tests/fuzzingserver-test.sh
new file mode 100755
index 0000000..af89a42
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-test.sh
@@ -0,0 +1,28 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+which wstest
+if [ $? != 0 ]; then
+ echo "Run 'pip install autobahntestsuite', maybe with 'sudo'."
+ exit 1
+fi
+which jq
+if [ $? != 0 ]; then
+ echo "Run 'brew install jq'"
+ exit 1
+fi
+
+trap 'kill $(jobs -pr)' SIGINT SIGTERM EXIT
+
+set -ex
+
+wstest -m fuzzingserver -s fuzzingserver-config.json &
+sleep 2 # wait for wstest to start
+
+java -jar target/okhttp-ws-tests-*-jar-with-dependencies.jar
+
+jq '.[] as $in | $in | keys[] | . + " " + $in[.].behavior' target/fuzzingserver-report/index.json > target/fuzzingserver-actual.txt
+
+diff fuzzingserver-expected.txt target/fuzzingserver-actual.txt
diff --git a/okhttp-ws-tests/fuzzingserver-update-expected.sh b/okhttp-ws-tests/fuzzingserver-update-expected.sh
new file mode 100755
index 0000000..56592c9
--- /dev/null
+++ b/okhttp-ws-tests/fuzzingserver-update-expected.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+SCRIPT_DIR="$( cd "$( dirname "$0" )" && pwd )"
+cd "$SCRIPT_DIR"
+
+if [ ! -f target/fuzzingserver-actual.txt ]; then
+ echo "File not found. Did you run the Autobahn test script?"
+ exit 1
+fi
+
+cp target/fuzzingserver-actual.txt fuzzingserver-expected.txt
diff --git a/okhttp-ws-tests/pom.xml b/okhttp-ws-tests/pom.xml
index c7d1778..1802304 100644
--- a/okhttp-ws-tests/pom.xml
+++ b/okhttp-ws-tests/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-ws-tests</artifactId>
@@ -15,14 +15,15 @@
<dependencies>
<dependency>
<groupId>com.squareup.okhttp</groupId>
- <artifactId>okhttp-testing-support</artifactId>
+ <artifactId>okhttp-ws</artifactId>
<version>${project.version}</version>
- <scope>test</scope>
</dependency>
+
<dependency>
<groupId>com.squareup.okhttp</groupId>
- <artifactId>okhttp-ws</artifactId>
+ <artifactId>okhttp-testing-support</artifactId>
<version>${project.version}</version>
+ <scope>test</scope>
</dependency>
<dependency>
@@ -40,6 +41,28 @@
<build>
<plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ <archive>
+ <manifest>
+ <mainClass>com.squareup.okhttp.ws.AutobahnTester</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
<!-- Do not deploy this as an artifact to Maven central. -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java b/okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
index a592624..08f7beb 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/AutobahnTester.java
+++ b/okhttp-ws-tests/src/main/java/com/squareup/okhttp/ws/AutobahnTester.java
@@ -17,7 +17,9 @@ package com.squareup.okhttp.ws;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.Version;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
@@ -29,6 +31,9 @@ import java.util.concurrent.atomic.AtomicReference;
import okio.Buffer;
import okio.BufferedSource;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
+
/**
* Exercises the web socket implementation against the
* <a href="http://autobahn.ws/testsuite/">Autobahn Testsuite</a>.
@@ -64,28 +69,34 @@ public final class AutobahnTester {
private void runTest(final long number, final long count) throws IOException {
final CountDownLatch latch = new CountDownLatch(1);
- newWebSocket("/runCase?case=" + number + "&agent=" + Version.userAgent()) //
+ final AtomicLong startNanos = new AtomicLong();
+ newWebSocket("/runCase?case=" + number + "&agent=okhttp") //
.enqueue(new WebSocketListener() {
private final ExecutorService sendExecutor = Executors.newSingleThreadExecutor();
private WebSocket webSocket;
@Override public void onOpen(WebSocket webSocket, Response response) {
- System.out.println("Executing test case " + number + "/" + count);
this.webSocket = webSocket;
- }
- @Override public void onMessage(BufferedSource payload, final WebSocket.PayloadType type)
- throws IOException {
- final Buffer buffer = new Buffer();
- payload.readAll(buffer);
- payload.close();
+ System.out.println("Executing test case " + number + "/" + count);
+ startNanos.set(System.nanoTime());
+ }
+ @Override public void onMessage(final ResponseBody message) throws IOException {
+ final RequestBody response;
+ if (message.contentType() == TEXT) {
+ response = RequestBody.create(TEXT, message.string());
+ } else {
+ BufferedSource source = message.source();
+ response = RequestBody.create(BINARY, source.readByteString());
+ source.close();
+ }
sendExecutor.execute(new Runnable() {
@Override public void run() {
try {
- webSocket.sendMessage(type, buffer);
+ webSocket.sendMessage(response);
} catch (IOException e) {
- e.printStackTrace();
+ e.printStackTrace(System.out);
}
}
});
@@ -100,16 +111,21 @@ public final class AutobahnTester {
}
@Override public void onFailure(IOException e, Response response) {
+ e.printStackTrace(System.out);
latch.countDown();
}
});
try {
- if (!latch.await(10, TimeUnit.SECONDS)) {
- throw new IllegalStateException("Timed out waiting for count.");
+ if (!latch.await(30, TimeUnit.SECONDS)) {
+ throw new IllegalStateException("Timed out waiting for test " + number + " to finish.");
}
} catch (InterruptedException e) {
throw new AssertionError();
}
+
+ long endNanos = System.nanoTime();
+ long tookMs = TimeUnit.NANOSECONDS.toMillis(endNanos - startNanos.get());
+ System.out.println("Took " + tookMs + "ms");
}
private long getTestCount() throws IOException {
@@ -120,10 +136,9 @@ public final class AutobahnTester {
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
- countRef.set(payload.readDecimalLong());
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ countRef.set(message.source().readDecimalLong());
+ message.close();
}
@Override public void onPong(Buffer payload) {
@@ -158,8 +173,7 @@ public final class AutobahnTester {
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
+ @Override public void onMessage(ResponseBody message) throws IOException {
}
@Override public void onPong(Buffer payload) {
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
index 241376d..8da32b3 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/RealWebSocketTest.java
@@ -15,25 +15,31 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.ws.WebSocketRecorder;
+import java.io.Closeable;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Random;
-import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
import okio.Buffer;
import okio.BufferedSink;
+import okio.BufferedSource;
import okio.ByteString;
+import okio.Okio;
+import okio.Sink;
+import okio.Source;
+import okio.Timeout;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@@ -42,34 +48,43 @@ public final class RealWebSocketTest {
// zero effect on the behavior of the WebSocket API which is why tests are only written once
// from the perspective of a single peer.
- private final Executor clientExecutor = Executors.newSingleThreadExecutor();
+ private final Executor clientExecutor = new SynchronousExecutor();
private RealWebSocket client;
private boolean clientConnectionCloseThrows;
private boolean clientConnectionClosed;
- private final Buffer client2Server = new Buffer();
+ private final MemorySocket client2Server = new MemorySocket();
private final WebSocketRecorder clientListener = new WebSocketRecorder();
- private final Executor serverExecutor = Executors.newSingleThreadExecutor();
+ private final Executor serverExecutor = new SynchronousExecutor();
private RealWebSocket server;
- private final Buffer server2client = new Buffer();
+ private boolean serverConnectionClosed;
+ private final MemorySocket server2client = new MemorySocket();
private final WebSocketRecorder serverListener = new WebSocketRecorder();
@Before public void setUp() {
Random random = new Random(0);
String url = "http://example.com/websocket";
- client = new RealWebSocket(true, server2client, client2Server, random, clientExecutor,
- clientListener, url) {
- @Override protected void closeConnection() throws IOException {
+ client = new RealWebSocket(true, server2client.source(), client2Server.sink(), random,
+ clientExecutor, clientListener, url) {
+ @Override protected void close() throws IOException {
+ if (clientConnectionClosed) {
+ throw new AssertionError("Already closed");
+ }
clientConnectionClosed = true;
+
if (clientConnectionCloseThrows) {
throw new IOException("Oops!");
}
}
};
- server = new RealWebSocket(false, client2Server, server2client, random, serverExecutor,
- serverListener, url) {
- @Override protected void closeConnection() throws IOException {
+ server = new RealWebSocket(false, client2Server.source(), server2client.sink(), random,
+ serverExecutor, serverListener, url) {
+ @Override protected void close() throws IOException {
+ if (serverConnectionClosed) {
+ throw new AssertionError("Already closed");
+ }
+ serverConnectionClosed = true;
}
};
}
@@ -79,36 +94,82 @@ public final class RealWebSocketTest {
serverListener.assertExhausted();
}
+ @Test public void nullMessageThrows() throws IOException {
+ try {
+ client.sendMessage(null);
+ fail();
+ } catch (NullPointerException e) {
+ assertEquals("message == null", e.getMessage());
+ }
+ }
+
@Test public void textMessage() throws IOException {
- client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
server.readMessage();
serverListener.assertTextMessage("Hello!");
}
@Test public void binaryMessage() throws IOException {
- client.sendMessage(BINARY, new Buffer().writeUtf8("Hello!"));
+ client.sendMessage(RequestBody.create(BINARY, "Hello!"));
server.readMessage();
serverListener.assertBinaryMessage(new byte[] { 'H', 'e', 'l', 'l', 'o', '!' });
}
+ @Test public void missingContentTypeThrows() throws IOException {
+ try {
+ client.sendMessage(RequestBody.create(null, "Hey!"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals("Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.",
+ e.getMessage());
+ }
+ }
+
+ @Test public void unknownContentTypeThrows() throws IOException {
+ try {
+ client.sendMessage(RequestBody.create(MediaType.parse("text/plain"), "Hey!"));
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertEquals(
+ "Unknown message content type: text/plain. Must use WebSocket.TEXT or WebSocket.BINARY.",
+ e.getMessage());
+ }
+ }
+
@Test public void streamingMessage() throws IOException {
- BufferedSink sink = client.newMessageSink(TEXT);
- sink.writeUtf8("Hel").flush();
- sink.writeUtf8("lo!").flush();
- sink.close();
+ RequestBody message = new RequestBody() {
+ @Override public MediaType contentType() {
+ return TEXT;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Hel").flush();
+ sink.writeUtf8("lo!").flush();
+ sink.close();
+ }
+ };
+ client.sendMessage(message);
server.readMessage();
serverListener.assertTextMessage("Hello!");
}
@Test public void streamingMessageCanInterleavePing() throws IOException, InterruptedException {
- BufferedSink sink = client.newMessageSink(TEXT);
- sink.writeUtf8("Hel").flush();
- client.sendPing(new Buffer().writeUtf8("Pong?"));
- sink.writeUtf8("lo!").flush();
- sink.close();
+ RequestBody message = new RequestBody() {
+ @Override public MediaType contentType() {
+ return TEXT;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ sink.writeUtf8("Hel").flush();
+ client.sendPing(new Buffer().writeUtf8("Pong?"));
+ sink.writeUtf8("lo!").flush();
+ sink.close();
+ }
+ };
+
+ client.sendMessage(message);
server.readMessage();
serverListener.assertTextMessage("Hello!");
- waitForExecutor(serverExecutor); // Pong write happens asynchronously.
client.readMessage();
clientListener.assertPong(new Buffer().writeUtf8("Pong?"));
}
@@ -116,7 +177,6 @@ public final class RealWebSocketTest {
@Test public void pingWritesPong() throws IOException, InterruptedException {
client.sendPing(new Buffer().writeUtf8("Hello!"));
server.readMessage(); // Read the ping, write the pong.
- waitForExecutor(serverExecutor); // Pong write happens asynchronously.
client.readMessage(); // Read the pong.
clientListener.assertPong(new Buffer().writeUtf8("Hello!"));
}
@@ -151,82 +211,138 @@ public final class RealWebSocketTest {
assertEquals("closed", e.getMessage());
}
try {
- client.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
fail();
} catch (IllegalStateException e) {
assertEquals("closed", e.getMessage());
}
+ }
+
+ @Test public void socketClosedDuringPingKillsWebSocket() throws IOException {
+ client2Server.close();
+
+ try {
+ client.sendPing(new Buffer().writeUtf8("Ping!"));
+ fail();
+ } catch (IOException ignored) {
+ }
+
+ // A failed write prevents further use of the WebSocket instance.
try {
- client.newMessageSink(TEXT);
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
fail();
} catch (IllegalStateException e) {
- assertEquals("closed", e.getMessage());
+ assertEquals("must call close()", e.getMessage());
+ }
+ try {
+ client.sendPing(new Buffer().writeUtf8("Ping!"));
+ fail();
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
}
}
- @Test public void serverCloseThenWritingThrows() throws IOException {
- server.close(1000, "Hello!");
- client.readMessage();
- clientListener.assertClose(1000, "Hello!");
+ @Test public void socketClosedDuringMessageKillsWebSocket() throws IOException {
+ client2Server.close();
try {
- client.sendPing(new Buffer().writeUtf8("Pong?"));
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
fail();
- } catch (IOException e) {
- assertEquals("closed", e.getMessage());
+ } catch (IOException ignored) {
}
+
+ // A failed write prevents further use of the WebSocket instance.
try {
- client.sendMessage(TEXT, new Buffer().writeUtf8("Hi!"));
+ client.sendMessage(RequestBody.create(TEXT, "Hello!"));
fail();
- } catch (IOException e) {
- assertEquals("closed", e.getMessage());
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
}
try {
- client.close(1000, "Bye!");
+ client.sendPing(new Buffer().writeUtf8("Ping!"));
fail();
- } catch (IOException e) {
- assertEquals("closed", e.getMessage());
+ } catch (IllegalStateException e) {
+ assertEquals("must call close()", e.getMessage());
}
}
- @Test public void serverCloseWhileWritingThrows() throws IOException {
- // Start writing data.
- BufferedSink sink = client.newMessageSink(TEXT);
- sink.writeUtf8("Hel").flush();
-
+ @Test public void serverCloseThenWritingPingThrows() throws IOException {
server.close(1000, "Hello!");
client.readMessage();
clientListener.assertClose(1000, "Hello!");
try {
- sink.writeUtf8("lo!").emit(); // No writing to the underlying sink.
+ client.sendPing(new Buffer().writeUtf8("Pong?"));
fail();
} catch (IOException e) {
assertEquals("closed", e.getMessage());
- sink.buffer().clear();
}
+ }
+
+ @Test public void serverCloseThenWritingMessageThrows() throws IOException {
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
try {
- sink.flush(); // No flushing.
+ client.sendMessage(RequestBody.create(TEXT, "Hi!"));
fail();
} catch (IOException e) {
assertEquals("closed", e.getMessage());
}
+ }
+
+ @Test public void serverCloseThenWritingCloseThrows() throws IOException {
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
try {
- sink.close(); // No closing because this requires writing a frame.
+ client.close(1000, "Bye!");
fail();
} catch (IOException e) {
assertEquals("closed", e.getMessage());
}
}
+ @Test public void serverCloseWhileWritingThrows() throws IOException {
+ RequestBody message = new RequestBody() {
+ @Override public MediaType contentType() {
+ return TEXT;
+ }
+
+ @Override public void writeTo(BufferedSink sink) throws IOException {
+ // Start writing data.
+ sink.writeUtf8("Hel").flush();
+
+ server.close(1000, "Hello!");
+ client.readMessage();
+ clientListener.assertClose(1000, "Hello!");
+
+ try {
+ sink.flush(); // No flushing.
+ fail();
+ } catch (IOException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ try {
+ sink.close(); // No closing because this requires writing a frame.
+ fail();
+ } catch (IOException e) {
+ assertEquals("closed", e.getMessage());
+ }
+ }
+ };
+ client.sendMessage(message);
+ }
+
@Test public void clientCloseClosesConnection() throws IOException {
client.close(1000, "Hello!");
assertFalse(clientConnectionClosed);
server.readMessage(); // Read client close, send server close.
serverListener.assertClose(1000, "Hello!");
- client.readMessage(); // Read server close.
- waitForExecutor(clientExecutor); // Close happens asynchronously.
+ client.readMessage(); // Read server close, close connection.
assertTrue(clientConnectionClosed);
clientListener.assertClose(1000, "Hello!");
}
@@ -235,8 +351,8 @@ public final class RealWebSocketTest {
server.close(1000, "Hello!");
client.readMessage(); // Read server close, send client close, close connection.
- clientListener.assertClose(1000, "Hello!");
assertTrue(clientConnectionClosed);
+ clientListener.assertClose(1000, "Hello!");
server.readMessage();
serverListener.assertClose(1000, "Hello!");
@@ -248,8 +364,7 @@ public final class RealWebSocketTest {
client.close(1000, "Hi!");
assertFalse(clientConnectionClosed);
- client.readMessage(); // Read close, should NOT send close.
- waitForExecutor(clientExecutor); // Close happens asynchronously.
+ client.readMessage(); // Read close, close connection close.
assertTrue(clientConnectionClosed);
clientListener.assertClose(1000, "Hello!");
@@ -261,7 +376,7 @@ public final class RealWebSocketTest {
}
@Test public void serverCloseBreaksReadMessageLoop() throws IOException {
- server.sendMessage(TEXT, new Buffer().writeUtf8("Hello!"));
+ server.sendMessage(RequestBody.create(TEXT, "Hello!"));
server.close(1000, "Bye!");
assertTrue(client.readMessage());
clientListener.assertTextMessage("Hello!");
@@ -269,12 +384,12 @@ public final class RealWebSocketTest {
clientListener.assertClose(1000, "Bye!");
}
- @Test public void protocolErrorBeforeCloseSendsClose() {
- server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+ @Test public void protocolErrorBeforeCloseSendsClose() throws IOException {
+ server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
- client.readMessage(); // Detects error, send close.
- clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+ client.readMessage(); // Detects error, send close, close connection.
assertTrue(clientConnectionClosed);
+ clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
server.readMessage();
serverListener.assertClose(1002, "");
@@ -282,14 +397,41 @@ public final class RealWebSocketTest {
@Test public void protocolErrorAfterCloseDoesNotSendClose() throws IOException {
client.close(1000, "Hello!");
- server2client.write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
+ assertFalse(clientConnectionClosed); // Not closed until close reply is received.
+ server2client.raw().write(ByteString.decodeHex("0a00")); // Invalid non-final ping frame.
- client.readMessage();
- clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
+ client.readMessage(); // Detects error, closes connection immediately since close already sent.
assertTrue(clientConnectionClosed);
+ clientListener.assertFailure(ProtocolException.class, "Control frames must be final.");
server.readMessage();
serverListener.assertClose(1000, "Hello!");
+
+ serverListener.assertExhausted(); // Client should not have sent second close.
+ }
+
+ @Test public void closeThrowingClosesConnection() {
+ client2Server.close();
+
+ try {
+ client.close(1000, null);
+ fail();
+ } catch (IOException ignored) {
+ }
+ assertTrue(clientConnectionClosed);
+ }
+
+ @Test public void closeMessageAndConnectionCloseThrowingDoesNotMaskOriginal() throws IOException {
+ client2Server.close();
+ clientConnectionCloseThrows = true;
+
+ try {
+ client.close(1000, "Bye!");
+ fail();
+ } catch (IOException e) {
+ assertNotEquals("Oops!", e.getMessage());
+ }
+ assertTrue(clientConnectionClosed);
}
@Test public void peerConnectionCloseThrowingDoesNotPropagate() throws IOException {
@@ -297,26 +439,66 @@ public final class RealWebSocketTest {
server.close(1000, "Bye!");
client.readMessage();
- clientListener.assertClose(1000, "Bye!");
assertTrue(clientConnectionClosed);
+ clientListener.assertClose(1000, "Bye!");
server.readMessage();
serverListener.assertClose(1000, "Bye!");
}
- private static void waitForExecutor(Executor executor) {
- final CountDownLatch latch = new CountDownLatch(1);
- executor.execute(new Runnable() {
- @Override public void run() {
- latch.countDown();
- }
- });
- try {
- if (!latch.await(10, TimeUnit.SECONDS)) {
- throw new IllegalStateException("Timed out waiting for executor.");
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ static final class MemorySocket implements Closeable {
+ private final Buffer buffer = new Buffer();
+ private boolean closed;
+
+ @Override public void close() {
+ closed = true;
+ }
+
+ Buffer raw() {
+ return buffer;
+ }
+
+ BufferedSource source() {
+ return Okio.buffer(new Source() {
+ @Override public long read(Buffer sink, long byteCount) throws IOException {
+ if (closed) throw new IOException("closed");
+ return buffer.read(sink, byteCount);
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
+ closed = true;
+ }
+ });
+ }
+
+ BufferedSink sink() {
+ return Okio.buffer(new Sink() {
+ @Override public void write(Buffer source, long byteCount) throws IOException {
+ if (closed) throw new IOException("closed");
+ buffer.write(source, byteCount);
+ }
+
+ @Override public void flush() throws IOException {
+ }
+
+ @Override public Timeout timeout() {
+ return Timeout.NONE;
+ }
+
+ @Override public void close() throws IOException {
+ closed = true;
+ }
+ });
+ }
+ }
+
+ static final class SynchronousExecutor implements Executor {
+ @Override public void execute(Runnable command) {
+ command.run();
}
}
}
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
index 1674511..213bda5 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketReaderTest.java
@@ -15,22 +15,24 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.ws.WebSocketRecorder;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.Random;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
import org.junit.After;
import org.junit.Test;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
import static com.squareup.okhttp.ws.WebSocketRecorder.MessageDelegate;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class WebSocketReaderTest {
@@ -151,11 +153,12 @@ public final class WebSocketReaderTest {
final Buffer sink = new Buffer();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.readFully(sink, 3); // Read "Hel"
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ BufferedSource source = message.source();
+ source.readFully(sink, 3); // Read "Hel"
data.write(ByteString.decodeHex("5158")); // lo
- payload.readFully(sink, 2); // Read "lo"
- payload.close();
+ source.readFully(sink, 2); // Read "lo"
+ source.close();
}
});
serverReader.processNextFrame();
@@ -251,8 +254,8 @@ public final class WebSocketReaderTest {
@Test public void noCloseErrors() throws IOException {
data.write(ByteString.decodeHex("810548656c6c6f")); // Hello
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.readAll(new Buffer());
+ @Override public void onMessage(ResponseBody body) throws IOException {
+ body.source().readAll(new Buffer());
}
});
try {
@@ -269,9 +272,9 @@ public final class WebSocketReaderTest {
final Buffer sink = new Buffer();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.read(sink, 3);
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ message.source().read(sink, 3);
+ message.close();
}
});
@@ -291,9 +294,9 @@ public final class WebSocketReaderTest {
final Buffer sink = new Buffer();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.read(sink, 2);
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ message.source().read(sink, 2);
+ message.close();
}
});
@@ -311,10 +314,10 @@ public final class WebSocketReaderTest {
final AtomicReference<Exception> exception = new AtomicReference<>();
callback.setNextMessageDelegate(new MessageDelegate() {
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- payload.close();
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ message.close();
try {
- payload.readAll(new Buffer());
+ message.source().readAll(new Buffer());
fail();
} catch (IllegalStateException e) {
exception.set(e);
@@ -341,7 +344,17 @@ public final class WebSocketReaderTest {
@Test public void emptyCloseCallsCallback() throws IOException {
data.write(ByteString.decodeHex("8800")); // Empty close
clientReader.processNextFrame();
- callback.assertClose(0, "");
+ callback.assertClose(1000, "");
+ }
+
+ @Test public void closeLengthOfOneThrows() throws IOException {
+ data.write(ByteString.decodeHex("880100")); // Close with invalid 1-byte payload
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Malformed close payload length of 1.", e.getMessage());
+ }
}
@Test public void closeCallsCallback() throws IOException {
@@ -350,6 +363,44 @@ public final class WebSocketReaderTest {
callback.assertClose(1000, "Hello");
}
+ @Test public void closeOutOfRangeThrows() throws IOException {
+ data.write(ByteString.decodeHex("88020001")); // Close with code 1
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Code must be in range [1000,5000): 1", e.getMessage());
+ }
+ data.write(ByteString.decodeHex("88021388")); // Close with code 5000
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ assertEquals("Code must be in range [1000,5000): 5000", e.getMessage());
+ }
+ }
+
+ @Test public void closeReservedSetThrows() throws IOException {
+ data.write(ByteString.decodeHex("880203ec")); // Close with code 1004
+ data.write(ByteString.decodeHex("880203ed")); // Close with code 1005
+ data.write(ByteString.decodeHex("880203ee")); // Close with code 1006
+ for (int i = 1012; i <= 2999; i++) {
+ data.write(ByteString.decodeHex("8802" + String.format("%04X", i))); // Close with code 'i'
+ }
+
+ int count = 0;
+ for (; !data.exhausted(); count++) {
+ try {
+ clientReader.processNextFrame();
+ fail();
+ } catch (ProtocolException e) {
+ String message = e.getMessage();
+ assertTrue(message, Pattern.matches("Code \\d+ is reserved and may not be used.", message));
+ }
+ }
+ assertEquals(1991, count);
+ }
+
private byte[] binaryData(int length) {
byte[] junk = new byte[length];
random.nextBytes(junk);
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
index a98e6bb..741b33f 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/internal/ws/WebSocketWriterTest.java
@@ -17,16 +17,22 @@ 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 okio.Okio;
+import okio.Sink;
+import org.junit.Rule;
import org.junit.Test;
-
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@@ -35,28 +41,27 @@ public final class WebSocketWriterTest {
private final Buffer data = new Buffer();
private final Random random = new Random(0);
+ /**
+ * Check all data as verified inside of the test. We do this in a rule instead of @After so that
+ * exceptions thrown from the test do not cause this check to fail.
+ */
+ @Rule public final TestRule noDataLeftBehind = new TestRule() {
+ @Override public Statement apply(final Statement base, Description description) {
+ return new Statement() {
+ @Override public void evaluate() throws Throwable {
+ base.evaluate();
+ assertEquals("Data not empty", "", data.readByteString().hex());
+ }
+ };
+ }
+ };
+
// Mutually exclusive. Use the one corresponding to the peer whose behavior you wish to test.
private final WebSocketWriter serverWriter = new WebSocketWriter(false, data, random);
private final WebSocketWriter clientWriter = new WebSocketWriter(true, data, random);
- @After public void tearDown() throws IOException {
- assertEquals("Data not empty", "", data.readByteString().hex());
- }
-
- @Test public void serverSendSimpleHello() throws IOException {
- Buffer payload = new Buffer().writeUtf8("Hello");
- serverWriter.sendMessage(TEXT, payload);
- assertData("810548656c6c6f");
- }
-
- @Test public void clientSendSimpleHello() throws IOException {
- Buffer payload = new Buffer().writeUtf8("Hello");
- clientWriter.sendMessage(TEXT, payload);
- assertData("818560b420bb28d14cd70f");
- }
-
- @Test public void serverStreamSimpleHello() throws IOException {
- BufferedSink sink = serverWriter.newMessageSink(TEXT);
+ @Test public void serverTextMessage() throws IOException {
+ BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_TEXT));
sink.writeUtf8("Hel").flush();
assertData("010348656c");
@@ -68,19 +73,34 @@ public final class WebSocketWriterTest {
assertData("8000");
}
- @Test public void serverStreamCloseFlushes() throws IOException {
- BufferedSink sink = serverWriter.newMessageSink(TEXT);
+ @Test public void closeFlushes() throws IOException {
+ BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_TEXT));
sink.writeUtf8("Hel").flush();
assertData("010348656c");
sink.writeUtf8("lo").close();
- assertData("00026c6f");
- assertData("8000");
+ assertData("80026c6f");
+ }
+
+ @Test public void noWritesAfterClose() throws IOException {
+ Sink sink = serverWriter.newMessageSink(OPCODE_TEXT);
+
+ sink.close();
+ assertData("8100");
+
+ Buffer payload = new Buffer().writeUtf8("Hello");
+ try {
+ // Write to the unbuffered sink as BufferedSink keeps its own closed state.
+ sink.write(payload, payload.size());
+ fail();
+ } catch (IOException e) {
+ assertEquals("closed", e.getMessage());
+ }
}
- @Test public void clientStreamSimpleHello() throws IOException {
- BufferedSink sink = clientWriter.newMessageSink(TEXT);
+ @Test public void clientTextMessage() throws IOException {
+ BufferedSink sink = Okio.buffer(clientWriter.newMessageSink(OPCODE_TEXT));
sink.writeUtf8("Hel").flush();
assertData("018360b420bb28d14c");
@@ -92,87 +112,84 @@ public final class WebSocketWriterTest {
assertData("80807acb933d");
}
- @Test public void serverSendBinary() throws IOException {
- byte[] payload = binaryData(100);
- serverWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("8264");
- assertData(payload);
- }
+ @Test public void serverBinaryMessage() throws IOException {
+ BufferedSink sink = Okio.buffer(serverWriter.newMessageSink(OPCODE_BINARY));
- @Test public void serverSendBinaryShort() throws IOException {
- byte[] payload = binaryData(0xffff);
- serverWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("827effff");
- assertData(payload);
- }
+ sink.write(binaryData(50)).flush();
+ assertData("0232");
+ assertData(binaryData(50));
- @Test public void serverSendBinaryLong() throws IOException {
- byte[] payload = binaryData(65537);
- serverWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("827f0000000000010001");
- assertData(payload);
+ sink.write(binaryData(50)).flush();
+ assertData("0032");
+ assertData(binaryData(50));
+
+ sink.close();
+ assertData("8000");
}
- @Test public void clientSendBinary() throws IOException {
- byte[] payload = binaryData(100);
- clientWriter.sendMessage(BINARY, new Buffer().write(payload));
- assertData("82e4");
+ @Test public void serverMessageLengthShort() throws IOException {
+ Sink sink = serverWriter.newMessageSink(OPCODE_BINARY);
+
+ // Create a payload which will overflow the normal payload byte size.
+ Buffer payload = new Buffer();
+ while (payload.completeSegmentByteCount() <= PAYLOAD_BYTE_MAX) {
+ payload.writeByte('0');
+ }
+ long byteCount = payload.completeSegmentByteCount();
- 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);
+ // Write directly to the unbuffered sink. This ensures it will become single frame.
+ sink.write(payload.clone(), byteCount);
+ assertData("027e"); // 'e' == 4-byte follow-up length.
+ assertData(String.format("%04X", payload.completeSegmentByteCount()));
+ assertData(payload.readByteArray());
- assertData(maskKey);
- assertData(payload);
+ sink.close();
+ assertData("8000");
}
- @Test public void serverStreamBinary() throws IOException {
- byte[] payload = binaryData(100);
- BufferedSink sink = serverWriter.newMessageSink(BINARY);
+ @Test public void serverMessageLengthLong() throws IOException {
+ Sink sink = serverWriter.newMessageSink(OPCODE_BINARY);
- sink.write(payload, 0, 50).flush();
- assertData("0232");
- assertData(Arrays.copyOfRange(payload, 0, 50));
+ // Create a payload which will overflow the normal and short payload byte size.
+ Buffer payload = new Buffer();
+ while (payload.completeSegmentByteCount() <= PAYLOAD_SHORT_MAX) {
+ payload.writeByte('0');
+ }
+ long byteCount = payload.completeSegmentByteCount();
- sink.write(payload, 50, 50).flush();
- assertData("0032");
- assertData(Arrays.copyOfRange(payload, 50, 100));
+ // Write directly to the unbuffered sink. This ensures it will become single frame.
+ sink.write(payload.clone(), byteCount);
+ assertData("027f"); // 'f' == 16-byte follow-up length.
+ assertData(String.format("%016X", byteCount));
+ assertData(payload.readByteArray(byteCount));
sink.close();
assertData("8000");
}
- @Test public void clientStreamBinary() throws IOException {
+ @Test public void clientBinary() throws IOException {
byte[] maskKey1 = new byte[4];
random.nextBytes(maskKey1);
byte[] maskKey2 = new byte[4];
random.nextBytes(maskKey2);
- byte[] maskKey3 = new byte[4];
- random.nextBytes(maskKey3);
random.setSeed(0); // Reset the seed so real data matches.
- byte[] payload = binaryData(100);
- BufferedSink sink = clientWriter.newMessageSink(BINARY);
+ BufferedSink sink = Okio.buffer(clientWriter.newMessageSink(OPCODE_BINARY));
- sink.write(payload, 0, 50).flush();
- byte[] part1 = Arrays.copyOfRange(payload, 0, 50);
+ byte[] part1 = binaryData(50);
+ sink.write(part1).flush();
toggleMask(part1, 50, maskKey1, 0);
assertData("02b2");
assertData(maskKey1);
assertData(part1);
- sink.write(payload, 50, 50).flush();
- byte[] part2 = Arrays.copyOfRange(payload, 50, 100);
+ byte[] part2 = binaryData(50);
+ sink.write(part2).close();
toggleMask(part2, 50, maskKey2, 0);
- assertData("00b2");
+ assertData("80b2");
assertData(maskKey2);
assertData(part2);
-
- sink.close();
- assertData("8080");
- assertData(maskKey3);
}
@Test public void serverEmptyClose() throws IOException {
@@ -287,26 +304,16 @@ public final class WebSocketWriterTest {
}
}
- @Test public void twoWritersThrows() {
- clientWriter.newMessageSink(TEXT);
+ @Test public void twoMessageSinksThrows() {
+ clientWriter.newMessageSink(OPCODE_TEXT);
try {
- clientWriter.newMessageSink(TEXT);
+ clientWriter.newMessageSink(OPCODE_TEXT);
fail();
} catch (IllegalStateException e) {
assertEquals("Another message writer is active. Did you call close()?", e.getMessage());
}
}
- @Test public void writeWhileWriterThrows() throws IOException {
- clientWriter.newMessageSink(TEXT);
- try {
- clientWriter.sendMessage(TEXT, new Buffer());
- fail();
- } catch (IllegalStateException e) {
- assertEquals("A message writer is active. Did you call close()?", e.getMessage());
- }
- }
-
private void assertData(String hex) throws EOFException {
ByteString expected = ByteString.decodeHex(hex);
ByteString actual = data.readByteString(expected.size());
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
index 895eb1f..bbc908c 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketCallTest.java
@@ -17,7 +17,9 @@ package com.squareup.okhttp.ws;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.SslContextBuilder;
import com.squareup.okhttp.mockwebserver.MockResponse;
import com.squareup.okhttp.mockwebserver.MockWebServer;
@@ -30,13 +32,11 @@ import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.SSLContext;
import okio.Buffer;
-import okio.BufferedSink;
-import okio.BufferedSource;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
public final class WebSocketCallTest {
@Rule public final MockWebServer server = new MockWebServer();
@@ -64,7 +64,7 @@ public final class WebSocketCallTest {
server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
WebSocket webSocket = awaitWebSocket();
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
+ webSocket.sendMessage(RequestBody.create(TEXT, "Hello, WebSockets!"));
serverListener.assertTextMessage("Hello, WebSockets!");
}
@@ -74,43 +74,7 @@ public final class WebSocketCallTest {
new Thread() {
@Override public void run() {
try {
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello, WebSockets!"));
- } catch (IOException e) {
- throw new AssertionError(e);
- }
- }
- }.start();
- }
- };
- server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
-
- awaitWebSocket();
- listener.assertTextMessage("Hello, WebSockets!");
- }
-
- @Test public void clientStreamingMessage() throws IOException {
- WebSocketRecorder serverListener = new WebSocketRecorder();
- server.enqueue(new MockResponse().withWebSocketUpgrade(serverListener));
-
- WebSocket webSocket = awaitWebSocket();
- BufferedSink sink = webSocket.newMessageSink(TEXT);
- sink.writeUtf8("Hello, ").flush();
- sink.writeUtf8("WebSockets!").flush();
- sink.close();
-
- serverListener.assertTextMessage("Hello, WebSockets!");
- }
-
- @Test public void serverStreamingMessage() throws IOException {
- WebSocketListener serverListener = new EmptyWebSocketListener() {
- @Override public void onOpen(final WebSocket webSocket, Response response) {
- new Thread() {
- @Override public void run() {
- try {
- BufferedSink sink = webSocket.newMessageSink(TEXT);
- sink.writeUtf8("Hello, ").flush();
- sink.writeUtf8("WebSockets!").flush();
- sink.close();
+ webSocket.sendMessage(RequestBody.create(TEXT, "Hello, WebSockets!"));
} catch (IOException e) {
throw new AssertionError(e);
}
@@ -233,7 +197,7 @@ public final class WebSocketCallTest {
.build();
WebSocket webSocket = awaitWebSocket(request1);
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("abc"));
+ webSocket.sendMessage(RequestBody.create(TEXT, "abc"));
serverListener.assertTextMessage("abc");
}
@@ -255,9 +219,8 @@ public final class WebSocketCallTest {
latch.countDown();
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
- listener.onMessage(payload, type);
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ listener.onMessage(message);
}
@Override public void onPong(Buffer payload) {
@@ -290,8 +253,7 @@ public final class WebSocketCallTest {
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource payload, WebSocket.PayloadType type)
- throws IOException {
+ @Override public void onMessage(ResponseBody message) throws IOException {
}
@Override public void onPong(Buffer payload) {
diff --git a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
index 56b3810..3e82e05 100644
--- a/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
+++ b/okhttp-ws-tests/src/test/java/com/squareup/okhttp/ws/WebSocketRecorder.java
@@ -15,24 +15,25 @@
*/
package com.squareup.okhttp.ws;
+import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.ws.WebSocketReader;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import okio.Buffer;
-import okio.BufferedSource;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public final class WebSocketRecorder implements WebSocketReader.FrameCallback, WebSocketListener {
public interface MessageDelegate {
- void onMessage(BufferedSource payload, WebSocket.PayloadType type) throws IOException;
+ void onMessage(ResponseBody message) throws IOException;
}
private final BlockingQueue<Object> events = new LinkedBlockingQueue<>();
@@ -46,16 +47,15 @@ public final class WebSocketRecorder implements WebSocketReader.FrameCallback, W
@Override public void onOpen(WebSocket webSocket, Response response) {
}
- @Override public void onMessage(BufferedSource source, WebSocket.PayloadType type)
- throws IOException {
+ @Override public void onMessage(ResponseBody message) throws IOException {
if (delegate != null) {
- delegate.onMessage(source, type);
+ delegate.onMessage(message);
delegate = null;
} else {
- Message message = new Message(type);
- source.readAll(message.buffer);
- source.close();
- events.add(message);
+ Message event = new Message(message.contentType());
+ message.source().readAll(event.buffer);
+ message.close();
+ events.add(event);
}
}
@@ -87,28 +87,48 @@ public final class WebSocketRecorder implements WebSocketReader.FrameCallback, W
}
}
- public void assertTextMessage(String payload) {
+ public void assertTextMessage(String payload) throws IOException {
Message message = new Message(TEXT);
message.buffer.writeUtf8(payload);
- assertEquals(message, nextEvent());
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(message, actual);
}
- public void assertBinaryMessage(byte[] payload) {
+ public void assertBinaryMessage(byte[] payload) throws IOException {
Message message = new Message(BINARY);
message.buffer.write(payload);
- assertEquals(message, nextEvent());
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(message, actual);
}
- public void assertPing(Buffer payload) {
- assertEquals(new Ping(payload), nextEvent());
+ public void assertPing(Buffer payload) throws IOException {
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(new Ping(payload), actual);
}
- public void assertPong(Buffer payload) {
- assertEquals(new Pong(payload), nextEvent());
+ public void assertPong(Buffer payload) throws IOException {
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(new Pong(payload), actual);
}
- public void assertClose(int code, String reason) {
- assertEquals(new Close(code, reason), nextEvent());
+ public void assertClose(int code, String reason) throws IOException {
+ Object actual = nextEvent();
+ if (actual instanceof IOException) {
+ throw (IOException) actual;
+ }
+ assertEquals(new Close(code, reason), actual);
}
public void assertFailure(Class<? extends IOException> cls, String message) {
@@ -125,25 +145,25 @@ public final class WebSocketRecorder implements WebSocketReader.FrameCallback, W
}
private static class Message {
- public final WebSocket.PayloadType type;
+ public final MediaType mediaType;
public final Buffer buffer = new Buffer();
- private Message(WebSocket.PayloadType type) {
- this.type = type;
+ private Message(MediaType mediaType) {
+ this.mediaType = mediaType;
}
@Override public String toString() {
- return "Message[" + type + " " + buffer + "]";
+ return "Message[" + mediaType + " " + buffer + "]";
}
@Override public int hashCode() {
- return type.hashCode() * 37 + buffer.hashCode();
+ return mediaType.hashCode() * 37 + buffer.hashCode();
}
@Override public boolean equals(Object obj) {
if (obj instanceof Message) {
Message other = (Message) obj;
- return type == other.type && buffer.equals(other.buffer);
+ return mediaType.equals(other.mediaType) && buffer.equals(other.buffer);
}
return false;
}
diff --git a/okhttp-ws/pom.xml b/okhttp-ws/pom.xml
index 81f8afd..688b538 100644
--- a/okhttp-ws/pom.xml
+++ b/okhttp-ws/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp-ws</artifactId>
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
index 8d6b7c4..ea55b5a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/RealWebSocket.java
@@ -15,6 +15,9 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.NamedRunnable;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketListener;
@@ -22,14 +25,17 @@ import java.io.IOException;
import java.net.ProtocolException;
import java.util.Random;
import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
+import okio.Okio;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
import static com.squareup.okhttp.internal.ws.WebSocketReader.FrameCallback;
public abstract class RealWebSocket implements WebSocket {
- /** A close code which indicates that the peer encountered a protocol exception. */
private static final int CLOSE_PROTOCOL_EXCEPTION = 1002;
private final WebSocketWriter writer;
@@ -38,10 +44,13 @@ public abstract class RealWebSocket implements WebSocket {
/** True after calling {@link #close(int, String)}. No writes are allowed afterward. */
private volatile boolean writerSentClose;
+ /** True after {@link IOException}. {@link #close(int, String)} becomes only valid call. */
+ private boolean writerWantsClose;
/** True after a close frame was read by the reader. No frames will follow it. */
- private volatile boolean readerSentClose;
- /** Lock required to negotiate closing the connection. */
- private final Object closeLock = new Object();
+ private boolean readerSentClose;
+
+ /** True after calling {@link #close()} to free connection resources. */
+ private final AtomicBoolean connectionClosed = new AtomicBoolean();
public RealWebSocket(boolean isClient, BufferedSource source, BufferedSink sink, Random random,
final Executor replyExecutor, final WebSocketListener listener, final String url) {
@@ -49,8 +58,8 @@ public abstract class RealWebSocket implements WebSocket {
writer = new WebSocketWriter(isClient, sink, random);
reader = new WebSocketReader(isClient, source, new FrameCallback() {
- @Override public void onMessage(BufferedSource source, PayloadType type) throws IOException {
- listener.onMessage(source, type);
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ listener.onMessage(message);
}
@Override public void onPing(final Buffer buffer) {
@@ -69,17 +78,10 @@ public abstract class RealWebSocket implements WebSocket {
}
@Override public void onClose(final int code, final String reason) {
- final boolean writeCloseResponse;
- synchronized (closeLock) {
- readerSentClose = true;
-
- // If the writer has not indicated a desire to close we will write a close response.
- writeCloseResponse = !writerSentClose;
- }
-
+ readerSentClose = true;
replyExecutor.execute(new NamedRunnable("OkHttp %s WebSocket Close Reply", url) {
@Override protected void execute() {
- peerClose(code, reason, writeCloseResponse);
+ peerClose(code, reason);
}
});
}
@@ -100,57 +102,96 @@ public abstract class RealWebSocket implements WebSocket {
}
}
- @Override public BufferedSink newMessageSink(PayloadType type) {
+ @Override public void sendMessage(RequestBody message) throws IOException {
+ if (message == null) throw new NullPointerException("message == null");
if (writerSentClose) throw new IllegalStateException("closed");
- return writer.newMessageSink(type);
- }
+ if (writerWantsClose) throw new IllegalStateException("must call close()");
- @Override public void sendMessage(PayloadType type, Buffer payload) throws IOException {
- if (writerSentClose) throw new IllegalStateException("closed");
- writer.sendMessage(type, payload);
+ MediaType contentType = message.contentType();
+ if (contentType == null) {
+ throw new IllegalArgumentException(
+ "Message content type was null. Must use WebSocket.TEXT or WebSocket.BINARY.");
+ }
+ String contentSubtype = contentType.subtype();
+
+ int formatOpcode;
+ if (WebSocket.TEXT.subtype().equals(contentSubtype)) {
+ formatOpcode = OPCODE_TEXT;
+ } else if (WebSocket.BINARY.subtype().equals(contentSubtype)) {
+ formatOpcode = OPCODE_BINARY;
+ } else {
+ throw new IllegalArgumentException("Unknown message content type: "
+ + contentType.type() + "/" + contentType.subtype() // Omit any implicitly added charset.
+ + ". Must use WebSocket.TEXT or WebSocket.BINARY.");
+ }
+
+ BufferedSink sink = Okio.buffer(writer.newMessageSink(formatOpcode));
+ try {
+ message.writeTo(sink);
+ sink.close();
+ } catch (IOException e) {
+ writerWantsClose = true;
+ throw e;
+ }
}
@Override public void sendPing(Buffer payload) throws IOException {
if (writerSentClose) throw new IllegalStateException("closed");
- writer.writePing(payload);
+ if (writerWantsClose) throw new IllegalStateException("must call close()");
+
+ try {
+ writer.writePing(payload);
+ } catch (IOException e) {
+ writerWantsClose = true;
+ throw e;
+ }
}
/** Send an unsolicited pong with the specified payload. */
public void sendPong(Buffer payload) throws IOException {
if (writerSentClose) throw new IllegalStateException("closed");
- writer.writePong(payload);
+ if (writerWantsClose) throw new IllegalStateException("must call close()");
+
+ try {
+ writer.writePong(payload);
+ } catch (IOException e) {
+ writerWantsClose = true;
+ throw e;
+ }
}
@Override public void close(int code, String reason) throws IOException {
if (writerSentClose) throw new IllegalStateException("closed");
+ writerSentClose = true;
- boolean closeConnection;
- synchronized (closeLock) {
- writerSentClose = true;
-
- // If the reader has also indicated a desire to close we will close the connection.
- closeConnection = readerSentClose;
- }
-
- writer.writeClose(code, reason);
-
- if (closeConnection) {
- closeConnection();
+ try {
+ writer.writeClose(code, reason);
+ } catch (IOException e) {
+ if (connectionClosed.compareAndSet(false, true)) {
+ // Try to close the connection without masking the original exception.
+ try {
+ close();
+ } catch (IOException ignored) {
+ }
+ }
+ throw e;
}
}
/** Replies and closes this web socket when a close frame is read from the peer. */
- private void peerClose(int code, String reason, boolean writeCloseResponse) {
- if (writeCloseResponse) {
+ private void peerClose(int code, String reason) {
+ if (!writerSentClose) {
try {
writer.writeClose(code, reason);
} catch (IOException ignored) {
}
}
- try {
- closeConnection();
- } catch (IOException ignored) {
+ if (connectionClosed.compareAndSet(false, true)) {
+ try {
+ close();
+ } catch (IOException ignored) {
+ }
}
listener.onClose(code, reason);
@@ -158,32 +199,24 @@ public abstract class RealWebSocket implements WebSocket {
/** Called on the reader thread when an error occurs. */
private void readerErrorClose(IOException e) {
- boolean writeCloseResponse;
- synchronized (closeLock) {
- readerSentClose = true;
-
- // If the writer has not closed we will close the connection.
- writeCloseResponse = !writerSentClose;
- }
-
- if (writeCloseResponse) {
- if (e instanceof ProtocolException) {
- // For protocol exceptions, try to inform the server of such.
- try {
- writer.writeClose(CLOSE_PROTOCOL_EXCEPTION, null);
- } catch (IOException ignored) {
- }
+ // For protocol exceptions, try to inform the server of such.
+ if (!writerSentClose && e instanceof ProtocolException) {
+ try {
+ writer.writeClose(CLOSE_PROTOCOL_EXCEPTION, null);
+ } catch (IOException ignored) {
}
}
- try {
- closeConnection();
- } catch (IOException ignored) {
+ if (connectionClosed.compareAndSet(false, true)) {
+ try {
+ close();
+ } catch (IOException ignored) {
+ }
}
listener.onFailure(e, null);
}
- /** Perform any tear-down work on the connection (close the socket, recycle, etc.). */
- protected abstract void closeConnection() throws IOException;
+ /** Perform any tear-down work (close the connection, shutdown executors). */
+ protected abstract void close() throws IOException;
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
index 2b93398..0778278 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketProtocol.java
@@ -68,14 +68,16 @@ public final class WebSocketProtocol {
static final int OPCODE_CONTROL_PONG = 0xa;
/**
- * Maximum length of frame payload. Larger payloads, if supported, can use the special values
- * {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}.
+ * Maximum length of frame payload. Larger payloads, if supported by the frame type, can use the
+ * special values {@link #PAYLOAD_SHORT} or {@link #PAYLOAD_LONG}.
*/
- static final int PAYLOAD_MAX = 125;
+ static final long PAYLOAD_BYTE_MAX = 125L;
/**
* Value for {@link #B1_MASK_LENGTH} which indicates the next two bytes are the unsigned length.
*/
static final int PAYLOAD_SHORT = 126;
+ /** Maximum length of a frame payload to be denoted as {@link #PAYLOAD_SHORT}. */
+ static final long PAYLOAD_SHORT_MAX = 0xffffL;
/**
* Value for {@link #B1_MASK_LENGTH} which indicates the next eight bytes are the unsigned
* length.
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
index ce548b1..d81785a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketReader.java
@@ -15,6 +15,9 @@
*/
package com.squareup.okhttp.internal.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.ResponseBody;
+import com.squareup.okhttp.ws.WebSocket;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
@@ -24,7 +27,6 @@ import okio.Okio;
import okio.Source;
import okio.Timeout;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_FIN;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV1;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_RSV2;
@@ -40,7 +42,7 @@ import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_P
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_FLAG_CONTROL;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_LONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
import static java.lang.Integer.toHexString;
@@ -50,7 +52,7 @@ import static java.lang.Integer.toHexString;
*/
public final class WebSocketReader {
public interface FrameCallback {
- void onMessage(BufferedSource source, PayloadType type) throws IOException;
+ void onMessage(ResponseBody body) throws IOException;
void onPing(Buffer buffer);
void onPong(Buffer buffer);
void onClose(int code, String reason);
@@ -145,8 +147,8 @@ public final class WebSocketReader {
}
frameBytesRead = 0;
- if (isControlFrame && frameLength > PAYLOAD_MAX) {
- throw new ProtocolException("Control frame must be less than " + PAYLOAD_MAX + "B.");
+ if (isControlFrame && frameLength > PAYLOAD_BYTE_MAX) {
+ throw new ProtocolException("Control frame must be less than " + PAYLOAD_BYTE_MAX + "B.");
}
if (isMasked) {
@@ -182,18 +184,23 @@ public final class WebSocketReader {
frameCallback.onPong(buffer);
break;
case OPCODE_CONTROL_CLOSE:
- int code = 0;
+ int code = 1000;
String reason = "";
if (buffer != null) {
- if (buffer.size() < 2) {
- throw new ProtocolException("Close payload must be at least two bytes.");
+ long bufferSize = buffer.size();
+ if (bufferSize == 1) {
+ throw new ProtocolException("Malformed close payload length of 1.");
+ } else if (bufferSize != 0) {
+ code = buffer.readShort();
+ if (code < 1000 || code >= 5000) {
+ throw new ProtocolException("Code must be in range [1000,5000): " + code);
+ }
+ if ((code >= 1004 && code <= 1006) || (code >= 1012 && code <= 2999)) {
+ throw new ProtocolException("Code " + code + " is reserved and may not be used.");
+ }
+
+ reason = buffer.readUtf8();
}
- code = buffer.readShort();
- if (code < 1000 || code >= 5000) {
- throw new ProtocolException("Code must be in range [1000,5000): " + code);
- }
-
- reason = buffer.readUtf8();
}
frameCallback.onClose(code, reason);
closed = true;
@@ -204,20 +211,35 @@ public final class WebSocketReader {
}
private void readMessageFrame() throws IOException {
- PayloadType type;
+ final MediaType type;
switch (opcode) {
case OPCODE_TEXT:
- type = PayloadType.TEXT;
+ type = WebSocket.TEXT;
break;
case OPCODE_BINARY:
- type = PayloadType.BINARY;
+ type = WebSocket.BINARY;
break;
default:
throw new ProtocolException("Unknown opcode: " + toHexString(opcode));
}
+ final BufferedSource source = Okio.buffer(framedMessageSource);
+ ResponseBody body = new ResponseBody() {
+ @Override public MediaType contentType() {
+ return type;
+ }
+
+ @Override public long contentLength() throws IOException {
+ return -1;
+ }
+
+ @Override public BufferedSource source() throws IOException {
+ return source;
+ }
+ };
+
messageClosed = false;
- frameCallback.onMessage(Okio.buffer(framedMessageSource), type);
+ frameCallback.onMessage(body);
if (!messageClosed) {
throw new IllegalStateException("Listener failed to call close on message payload.");
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
index fc5de75..feece7a 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/internal/ws/WebSocketWriter.java
@@ -20,42 +20,41 @@ import 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.ws.WebSocket.PayloadType;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B0_FLAG_FIN;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.B1_FLAG_MASK;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_BINARY;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTINUATION;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_CLOSE;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PING;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_CONTROL_PONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.OPCODE_TEXT;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_LONG;
-import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_MAX;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_BYTE_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT;
+import static com.squareup.okhttp.internal.ws.WebSocketProtocol.PAYLOAD_SHORT_MAX;
import static com.squareup.okhttp.internal.ws.WebSocketProtocol.toggleMask;
/**
* An <a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a>-compatible WebSocket frame writer.
* <p>
* This class is partially thread safe. Only a single "main" thread should be sending messages via
- * calls to {@link #newMessageSink} or {@link #sendMessage} as well as any calls to
- * {@link #writePing} or {@link #writeClose}. Other threads may call {@link #writePing},
- * {@link #writePong}, or {@link #writeClose} which will interleave on the wire with frames from
- * the main thread.
+ * calls to {@link #newMessageSink}, {@link #writePing}, or {@link #writeClose}. Other threads may
+ * call {@link #writePing}, {@link #writePong}, or {@link #writeClose} which will interleave on the
+ * wire with frames from the "main" sending thread.
*/
public final class WebSocketWriter {
private final boolean isClient;
- /** Writes must be guarded by synchronizing on this instance! */
- private final BufferedSink sink;
private final Random random;
+ /** Writes must be guarded by synchronizing on 'this'. */
+ private final BufferedSink sink;
+ /** Access must be guarded by synchronizing on 'this'. */
+ private boolean writerClosed;
+
+ private final Buffer buffer = new Buffer();
private final FrameSink frameSink = new FrameSink();
- private boolean closed;
private boolean activeWriter;
private final byte[] maskKey;
@@ -75,15 +74,15 @@ public final class WebSocketWriter {
/** Send a ping with the supplied {@code payload}. Payload may be {@code null} */
public void writePing(Buffer payload) throws IOException {
- synchronized (sink) {
- writeControlFrame(OPCODE_CONTROL_PING, payload);
+ synchronized (this) {
+ writeControlFrameSynchronized(OPCODE_CONTROL_PING, payload);
}
}
/** Send a pong with the supplied {@code payload}. Payload may be {@code null} */
public void writePong(Buffer payload) throws IOException {
- synchronized (sink) {
- writeControlFrame(OPCODE_CONTROL_PONG, payload);
+ synchronized (this) {
+ writeControlFrameSynchronized(OPCODE_CONTROL_PONG, payload);
}
}
@@ -108,21 +107,23 @@ public final class WebSocketWriter {
}
}
- synchronized (sink) {
- writeControlFrame(OPCODE_CONTROL_CLOSE, payload);
- closed = true;
+ synchronized (this) {
+ writeControlFrameSynchronized(OPCODE_CONTROL_CLOSE, payload);
+ writerClosed = true;
}
}
- private void writeControlFrame(int opcode, Buffer payload) throws IOException {
- if (closed) throw new IOException("closed");
+ private void writeControlFrameSynchronized(int opcode, Buffer payload) throws IOException {
+ assert Thread.holdsLock(this);
+
+ if (writerClosed) throw new IOException("closed");
int length = 0;
if (payload != null) {
length = (int) payload.size();
- if (length > PAYLOAD_MAX) {
+ if (length > PAYLOAD_BYTE_MAX) {
throw new IllegalArgumentException(
- "Payload size must be less than or equal to " + PAYLOAD_MAX);
+ "Payload size must be less than or equal to " + PAYLOAD_BYTE_MAX);
}
}
@@ -138,7 +139,7 @@ public final class WebSocketWriter {
sink.write(maskKey);
if (payload != null) {
- writeAllMasked(payload, length);
+ writeMaskedSynchronized(payload, length);
}
} else {
sink.writeByte(b1);
@@ -148,93 +149,70 @@ public final class WebSocketWriter {
}
}
- sink.flush();
+ sink.emit();
}
/**
* Stream a message payload as a series of frames. This allows control frames to be interleaved
* between parts of the message.
*/
- public BufferedSink newMessageSink(PayloadType type) {
- if (type == null) throw new NullPointerException("type == null");
+ public Sink newMessageSink(int formatOpcode) {
if (activeWriter) {
throw new IllegalStateException("Another message writer is active. Did you call close()?");
}
activeWriter = true;
- frameSink.payloadType = type;
+ // Reset FrameSink state for a new writer.
+ frameSink.formatOpcode = formatOpcode;
frameSink.isFirstFrame = true;
- return Okio.buffer(frameSink);
- }
+ frameSink.closed = false;
- /**
- * 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 */);
+ return frameSink;
}
- 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);
- }
- }
+ private void writeMessageFrameSynchronized(int formatOpcode, long byteCount, boolean isFirstFrame,
+ boolean isFinal) throws IOException {
+ assert Thread.holdsLock(this);
- synchronized (sink) {
- int b0 = opcode;
- if (isFinal) {
- b0 |= B0_FLAG_FIN;
- }
- sink.writeByte(b0);
+ if (writerClosed) throw new IOException("closed");
- int b1 = 0;
- if (isClient) {
- b1 |= B1_FLAG_MASK;
- random.nextBytes(maskKey);
- }
- if (byteCount <= PAYLOAD_MAX) {
- b1 |= (int) byteCount;
- sink.writeByte(b1);
- } else if (byteCount <= 0xffffL) { // Unsigned short.
- b1 |= PAYLOAD_SHORT;
- sink.writeByte(b1);
- sink.writeShort((int) byteCount);
- } else {
- b1 |= PAYLOAD_LONG;
- sink.writeByte(b1);
- sink.writeLong(byteCount);
- }
+ int b0 = isFirstFrame ? formatOpcode : OPCODE_CONTINUATION;
+ if (isFinal) {
+ b0 |= B0_FLAG_FIN;
+ }
+ sink.writeByte(b0);
- if (isClient) {
- sink.write(maskKey);
- writeAllMasked(source, byteCount);
- } else {
- sink.write(source, byteCount);
- }
+ int b1 = 0;
+ if (isClient) {
+ b1 |= B1_FLAG_MASK;
+ random.nextBytes(maskKey);
+ }
+ if (byteCount <= PAYLOAD_BYTE_MAX) {
+ b1 |= (int) byteCount;
+ sink.writeByte(b1);
+ } else if (byteCount <= PAYLOAD_SHORT_MAX) {
+ b1 |= PAYLOAD_SHORT;
+ sink.writeByte(b1);
+ sink.writeShort((int) byteCount);
+ } else {
+ b1 |= PAYLOAD_LONG;
+ sink.writeByte(b1);
+ sink.writeLong(byteCount);
+ }
- sink.flush();
+ if (isClient) {
+ sink.write(maskKey);
+ writeMaskedSynchronized(buffer, byteCount);
+ } else {
+ sink.write(buffer, byteCount);
}
+
+ sink.emit();
}
- private void writeAllMasked(BufferedSource source, long byteCount) throws IOException {
+ private void writeMaskedSynchronized(BufferedSource source, long byteCount) throws IOException {
+ assert Thread.holdsLock(this);
+
long written = 0;
while (written < byteCount) {
int toRead = (int) Math.min(byteCount, maskBuffer.length);
@@ -247,20 +225,31 @@ public final class WebSocketWriter {
}
private final class FrameSink implements Sink {
- private PayloadType payloadType;
+ private int formatOpcode;
private boolean isFirstFrame;
+ private boolean closed;
@Override public void write(Buffer source, long byteCount) throws IOException {
- writeFrame(payloadType, source, byteCount, isFirstFrame, false /* final */);
- isFirstFrame = false;
+ if (closed) throw new IOException("closed");
+
+ buffer.write(source, byteCount);
+
+ long emitCount = buffer.completeSegmentByteCount();
+ if (emitCount > 0) {
+ synchronized (WebSocketWriter.this) {
+ writeMessageFrameSynchronized(formatOpcode, emitCount, isFirstFrame, false /* final */);
+ }
+ isFirstFrame = false;
+ }
}
@Override public void flush() throws IOException {
if (closed) throw new IOException("closed");
- synchronized (sink) {
- sink.flush();
+ synchronized (WebSocketWriter.this) {
+ writeMessageFrameSynchronized(formatOpcode, buffer.size(), isFirstFrame, false /* final */);
}
+ isFirstFrame = false;
}
@Override public Timeout timeout() {
@@ -271,21 +260,10 @@ public final class WebSocketWriter {
@Override public void close() throws IOException {
if (closed) throw new IOException("closed");
- int length = 0;
-
- synchronized (sink) {
- sink.writeByte(B0_FLAG_FIN | OPCODE_CONTINUATION);
-
- if (isClient) {
- sink.writeByte(B1_FLAG_MASK | length);
- random.nextBytes(maskKey);
- sink.write(maskKey);
- } else {
- sink.writeByte(length);
- }
- sink.flush();
+ synchronized (WebSocketWriter.this) {
+ writeMessageFrameSynchronized(formatOpcode, buffer.size(), isFirstFrame, true /* final */);
}
-
+ closed = true;
activeWriter = false;
}
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
index 4cf2f42..a3eebe7 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocket.java
@@ -15,40 +15,35 @@
*/
package com.squareup.okhttp.ws;
+import com.squareup.okhttp.MediaType;
+import com.squareup.okhttp.RequestBody;
import java.io.IOException;
import okio.Buffer;
-import okio.BufferedSink;
/** Blocking interface to connect and write to a web socket. */
public interface WebSocket {
- /** The format of a message payload. */
- enum PayloadType {
- /** UTF8-encoded text data. */
- TEXT,
- /** Arbitrary binary data. */
- BINARY
- }
+ /** A {@link MediaType} indicating UTF-8 text frames should be used when sending the message. */
+ MediaType TEXT = MediaType.parse("application/vnd.okhttp.websocket+text; charset=utf-8");
+ /** A {@link MediaType} indicating binary frames should be used when sending the message. */
+ MediaType BINARY = MediaType.parse("application/vnd.okhttp.websocket+binary");
/**
- * Stream a message payload to the server of the specified {code type}.
- * <p>
- * You must call {@link BufferedSink#close() close()} to complete the message. Calls to
- * {@link BufferedSink#flush() flush()} write a frame fragment. The message may be empty.
+ * Send a message payload to the server.
*
- * @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}.
+ * <p>The {@linkplain RequestBody#contentType() content type} of {@code message} should be either
+ * {@link #TEXT} or {@link #BINARY}.
*
+ * @throws IOException if unable to write the message. Clients must call {@link #close} when this
+ * happens to ensure resources are cleaned up.
* @throws IllegalStateException if not connected, already closed, or another writer is active.
*/
- void sendMessage(WebSocket.PayloadType type, Buffer payload) throws IOException;
+ void sendMessage(RequestBody message) throws IOException;
/**
* Send a ping to the server with optional payload.
*
+ * @throws IOException if unable to write the ping. Clients must call {@link #close} when this
+ * happens to ensure resources are cleaned up.
* @throws IllegalStateException if already closed.
*/
void sendPing(Buffer payload) throws IOException;
@@ -62,6 +57,7 @@ public interface WebSocket {
* It is an error to call this method before calling close on an active writer. Calling this
* method more than once has no effect.
*
+ * @throws IOException if unable to write the close message. Resources will still be freed.
* @throws IllegalStateException if already closed.
*/
void close(int code, String reason) throws IOException;
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
index 46ee8a1..5950850 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketCall.java
@@ -17,12 +17,12 @@ package com.squareup.okhttp.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.Util;
+import com.squareup.okhttp.internal.http.StreamAllocation;
import com.squareup.okhttp.internal.ws.RealWebSocket;
import com.squareup.okhttp.internal.ws.WebSocketProtocol;
import java.io.IOException;
@@ -30,11 +30,9 @@ import java.net.ProtocolException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.Random;
-import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
-import okio.BufferedSink;
-import okio.BufferedSource;
import okio.ByteString;
import static java.util.concurrent.TimeUnit.SECONDS;
@@ -47,7 +45,6 @@ public final class WebSocketCall {
return new WebSocketCall(client, request);
}
- private final Request request;
private final Call call;
private final Random random;
private final String key;
@@ -78,7 +75,6 @@ public final class WebSocketCall {
.header("Sec-WebSocket-Key", key)
.header("Sec-WebSocket-Version", "13")
.build();
- this.request = request;
call = client.newCall(request);
}
@@ -118,11 +114,9 @@ public final class WebSocketCall {
call.cancel();
}
- private void createWebSocket(Response response, WebSocketListener listener)
- throws IOException {
+ private void createWebSocket(Response response, WebSocketListener listener) throws IOException {
if (response.code() != 101) {
- // TODO call.engine.releaseConnection();
- Internal.instance.callEngineReleaseConnection(call);
+ Util.closeQuietly(response.body());
throw new ProtocolException("Expected HTTP 101 response but was '"
+ response.code()
+ " "
@@ -150,21 +144,9 @@ public final class WebSocketCall {
+ "'");
}
- // TODO connection = call.engine.getConnection();
- Connection connection = Internal.instance.callEngineGetConnection(call);
- // TODO if (!connection.clearOwner()) {
- if (!Internal.instance.clearOwner(connection)) {
- throw new IllegalStateException("Unable to take ownership of connection.");
- }
-
- BufferedSource source = Internal.instance.connectionRawSource(connection);
- BufferedSink sink = Internal.instance.connectionRawSink(connection);
-
- final RealWebSocket webSocket =
- ConnectionWebSocket.create(response, connection, source, sink, random, listener);
-
- // TODO connection.setOwner(webSocket);
- Internal.instance.connectionSetOwner(connection, webSocket);
+ StreamAllocation streamAllocation = Internal.instance.callEngineGetStreamAllocation(call);
+ RealWebSocket webSocket = StreamWebSocket.create(
+ streamAllocation, response, random, listener);
listener.onOpen(webSocket, response);
@@ -173,30 +155,33 @@ public final class WebSocketCall {
}
// Keep static so that the WebSocketCall instance can be garbage collected.
- private static class ConnectionWebSocket extends RealWebSocket {
- static RealWebSocket create(Response response, Connection connection, BufferedSource source,
- BufferedSink sink, Random random, WebSocketListener listener) {
+ private static class StreamWebSocket extends RealWebSocket {
+ static RealWebSocket create(StreamAllocation streamAllocation, Response response,
+ Random random, WebSocketListener listener) {
String url = response.request().urlString();
ThreadPoolExecutor replyExecutor =
new ThreadPoolExecutor(1, 1, 1, SECONDS, new LinkedBlockingDeque<Runnable>(),
Util.threadFactory(String.format("OkHttp %s WebSocket", url), true));
replyExecutor.allowCoreThreadTimeOut(true);
- return new ConnectionWebSocket(connection, source, sink, random, replyExecutor, listener,
- url);
+ return new StreamWebSocket(streamAllocation, random, replyExecutor, listener, url);
}
- private final Connection connection;
+ private final StreamAllocation streamAllocation;
+ private final ExecutorService replyExecutor;
- private ConnectionWebSocket(Connection connection, BufferedSource source, BufferedSink sink,
- Random random, Executor replyExecutor, WebSocketListener listener, String url) {
- super(true /* is client */, source, sink, random, replyExecutor, listener, url);
- this.connection = connection;
+ private StreamWebSocket(StreamAllocation streamAllocation,
+ Random random, ExecutorService replyExecutor, WebSocketListener listener, String url) {
+ super(true /* is client */, streamAllocation.connection().source,
+ streamAllocation.connection().sink, random, replyExecutor, listener, url);
+ this.streamAllocation = streamAllocation;
+ this.replyExecutor = replyExecutor;
}
- @Override protected void closeConnection() throws IOException {
- // TODO connection.closeIfOwnedBy(this);
- Internal.instance.closeIfOwnedBy(connection, this);
+ @Override protected void close() throws IOException {
+ replyExecutor.shutdown();
+ streamAllocation.noNewStreams();
+ streamAllocation.streamFinished(streamAllocation.stream());
}
}
}
diff --git a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
index 8941b74..5a5a8b1 100644
--- a/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
+++ b/okhttp-ws/src/main/java/com/squareup/okhttp/ws/WebSocketListener.java
@@ -16,11 +16,9 @@
package com.squareup.okhttp.ws;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
import okio.Buffer;
-import okio.BufferedSource;
-
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
/** Listener for server-initiated messages on a connected {@link WebSocket}. */
public interface WebSocketListener {
@@ -50,8 +48,11 @@ public interface WebSocketListener {
* <p>Implementations <strong>must</strong> call {@code source.close()} before returning. This
* indicates completion of parsing the message payload and will consume any remaining bytes in
* the message.
+ *
+ * <p>The {@linkplain ResponseBody#contentType() content type} of {@code message} will be either
+ * {@link WebSocket#TEXT} or {@link WebSocket#BINARY} which indicates the format of the message.
*/
- void onMessage(BufferedSource payload, PayloadType type) throws IOException;
+ void onMessage(ResponseBody message) throws IOException;
/**
* Called when a server pong is received. This is usually a result of calling {@link
diff --git a/okhttp/pom.xml b/okhttp/pom.xml
index 5cd1187..3254b30 100644
--- a/okhttp/pom.xml
+++ b/okhttp/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>okhttp</artifactId>
@@ -17,6 +17,11 @@
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</dependency>
+ <dependency>
+ <groupId>com.google.android</groupId>
+ <artifactId>android</artifactId>
+ <scope>provided</scope>
+ </dependency>
</dependencies>
<build>
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Address.java b/okhttp/src/main/java/com/squareup/okhttp/Address.java
index 6f6ce08..9efdf28 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Address.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Address.java
@@ -30,57 +30,90 @@ import static com.squareup.okhttp.internal.Util.equal;
* this is the server's hostname and port. If an explicit proxy is requested (or
* {@linkplain Proxy#NO_PROXY no proxy} is explicitly requested), this also includes
* that proxy information. For secure connections the address also includes the
- * SSL socket factory and hostname verifier.
+ * SSL socket factory, hostname verifier, and certificate pinner.
*
* <p>HTTP requests that share the same {@code Address} may also share the same
* {@link Connection}.
*/
public final class Address {
- final Proxy proxy;
- final String uriHost;
- final int uriPort;
+ final HttpUrl url;
+ final Dns dns;
final SocketFactory socketFactory;
- final SSLSocketFactory sslSocketFactory;
- final HostnameVerifier hostnameVerifier;
- final CertificatePinner certificatePinner;
final Authenticator authenticator;
final List<Protocol> protocols;
final List<ConnectionSpec> connectionSpecs;
final ProxySelector proxySelector;
+ final Proxy proxy;
+ final SSLSocketFactory sslSocketFactory;
+ final HostnameVerifier hostnameVerifier;
+ final CertificatePinner certificatePinner;
- public Address(String uriHost, int uriPort, SocketFactory socketFactory,
+ public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,
SSLSocketFactory sslSocketFactory, HostnameVerifier hostnameVerifier,
CertificatePinner certificatePinner, Authenticator authenticator, Proxy proxy,
List<Protocol> protocols, List<ConnectionSpec> connectionSpecs, ProxySelector proxySelector) {
- if (uriHost == null) throw new NullPointerException("uriHost == null");
- if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort);
+ this.url = new HttpUrl.Builder()
+ .scheme(sslSocketFactory != null ? "https" : "http")
+ .host(uriHost)
+ .port(uriPort)
+ .build();
+
+ if (dns == null) throw new IllegalArgumentException("dns == null");
+ this.dns = dns;
+
+ if (socketFactory == null) throw new IllegalArgumentException("socketFactory == null");
+ this.socketFactory = socketFactory;
+
if (authenticator == null) throw new IllegalArgumentException("authenticator == null");
+ this.authenticator = authenticator;
+
if (protocols == null) throw new IllegalArgumentException("protocols == null");
+ this.protocols = Util.immutableList(protocols);
+
+ if (connectionSpecs == null) throw new IllegalArgumentException("connectionSpecs == null");
+ this.connectionSpecs = Util.immutableList(connectionSpecs);
+
if (proxySelector == null) throw new IllegalArgumentException("proxySelector == null");
+ this.proxySelector = proxySelector;
+
this.proxy = proxy;
- this.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. */
+ /**
+ * Returns a URL with the hostname and port of the origin server. The path, query, and fragment of
+ * this URL are always empty, since they are not significant for planning a route.
+ */
+ public HttpUrl url() {
+ return url;
+ }
+
+ /**
+ * Returns the hostname of the origin server.
+ *
+ * @deprecated prefer {@code address.url().host()}.
+ */
+ @Deprecated
public String getUriHost() {
- return uriHost;
+ return url.host();
}
/**
* Returns the port of the origin server; typically 80 or 443. Unlike
* may {@code getPort()} accessors, this method never returns -1.
+ *
+ * @deprecated prefer {@code address.url().port()}.
*/
+ @Deprecated
public int getUriPort() {
- return uriPort;
+ return url.port();
+ }
+
+ /** Returns the service that will be used to resolve IP addresses for hostnames. */
+ public Dns getDns() {
+ return dns;
}
/** Returns the socket factory for new connections. */
@@ -88,25 +121,7 @@ public final class Address {
return socketFactory;
}
- /**
- * Returns the SSL socket factory, or null if this is not an HTTPS
- * address.
- */
- public SSLSocketFactory getSslSocketFactory() {
- return sslSocketFactory;
- }
-
- /**
- * Returns the hostname verifier, or null if this is not an HTTPS
- * address.
- */
- public HostnameVerifier getHostnameVerifier() {
- return hostnameVerifier;
- }
-
- /**
- * Returns the client's authenticator. This method never returns null.
- */
+ /** Returns the client's authenticator. */
public Authenticator getAuthenticator() {
return authenticator;
}
@@ -124,14 +139,6 @@ public final class Address {
}
/**
- * Returns this address's explicitly-specified HTTP proxy, or null to
- * delegate to the {@linkplain #getProxySelector proxy selector}.
- */
- public Proxy getProxy() {
- return proxy;
- }
-
- /**
* Returns this address's proxy selector. Only used if the proxy is null. If none of this
* selector's proxies are reachable, a direct connection will be attempted.
*/
@@ -140,8 +147,24 @@ public final class Address {
}
/**
- * Returns this address's certificate pinner. Only used for secure connections.
+ * Returns this address's explicitly-specified HTTP proxy, or null to
+ * delegate to the {@linkplain #getProxySelector proxy selector}.
*/
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /** Returns the SSL socket factory, or null if this is not an HTTPS address. */
+ public SSLSocketFactory getSslSocketFactory() {
+ return sslSocketFactory;
+ }
+
+ /** Returns the hostname verifier, or null if this is not an HTTPS address. */
+ public HostnameVerifier getHostnameVerifier() {
+ return hostnameVerifier;
+ }
+
+ /** Returns this address's certificate pinner, or null if this is not an HTTPS address. */
public CertificatePinner getCertificatePinner() {
return certificatePinner;
}
@@ -149,32 +172,32 @@ public final class Address {
@Override public boolean equals(Object other) {
if (other instanceof Address) {
Address that = (Address) other;
- return equal(this.proxy, that.proxy)
- && this.uriHost.equals(that.uriHost)
- && this.uriPort == that.uriPort
+ return this.url.equals(that.url)
+ && this.dns.equals(that.dns)
+ && this.authenticator.equals(that.authenticator)
+ && this.protocols.equals(that.protocols)
+ && this.connectionSpecs.equals(that.connectionSpecs)
+ && this.proxySelector.equals(that.proxySelector)
+ && equal(this.proxy, that.proxy)
&& equal(this.sslSocketFactory, that.sslSocketFactory)
&& equal(this.hostnameVerifier, that.hostnameVerifier)
- && equal(this.certificatePinner, that.certificatePinner)
- && equal(this.authenticator, that.authenticator)
- && equal(this.protocols, that.protocols)
- && equal(this.connectionSpecs, that.connectionSpecs)
- && equal(this.proxySelector, that.proxySelector);
+ && equal(this.certificatePinner, that.certificatePinner);
}
return false;
}
@Override public int hashCode() {
int result = 17;
- result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
- result = 31 * result + uriHost.hashCode();
- result = 31 * result + uriPort;
- result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
- result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
- result = 31 * result + (certificatePinner != null ? certificatePinner.hashCode() : 0);
+ result = 31 * result + url.hashCode();
+ result = 31 * result + dns.hashCode();
result = 31 * result + authenticator.hashCode();
result = 31 * result + protocols.hashCode();
result = 31 * result + connectionSpecs.hashCode();
result = 31 * result + proxySelector.hashCode();
+ result = 31 * result + (proxy != null ? proxy.hashCode() : 0);
+ result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0);
+ result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0);
+ result = 31 * result + (certificatePinner != null ? certificatePinner.hashCode() : 0);
return result;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Call.java b/okhttp/src/main/java/com/squareup/okhttp/Call.java
index 33561ba..651bd0d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Call.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Call.java
@@ -19,6 +19,7 @@ import com.squareup.okhttp.internal.NamedRunnable;
import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.RequestException;
import com.squareup.okhttp.internal.http.RouteException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
import java.io.IOException;
import java.net.ProtocolException;
import java.util.logging.Level;
@@ -119,7 +120,15 @@ public class Call {
*/
public void cancel() {
canceled = true;
- if (engine != null) engine.disconnect();
+ if (engine != null) engine.cancel();
+ }
+
+ /**
+ * Returns true if this call has been either {@linkplain #execute() executed} or {@linkplain
+ * #enqueue(Callback) enqueued}. It is an error to execute a call more than once.
+ */
+ public synchronized boolean isExecuted() {
+ return executed;
}
public boolean isCanceled() {
@@ -172,7 +181,8 @@ public class Call {
// Do not signal the callback twice!
logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
} else {
- responseCallback.onFailure(engine.getRequest(), e);
+ Request request = engine == null ? originalRequest : engine.getRequest();
+ responseCallback.onFailure(request, e);
}
} finally {
client.getDispatcher().finished(this);
@@ -215,14 +225,22 @@ public class Call {
}
@Override public Response proceed(Request request) throws IOException {
+ // If there's another interceptor in the chain, call that.
if (index < client.interceptors().size()) {
- // There's another interceptor in the chain. Call that.
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
- return client.interceptors().get(index).intercept(chain);
- } else {
- // No more interceptors. Do HTTP.
- return getResponse(request, forWebSocket);
+ Interceptor interceptor = client.interceptors().get(index);
+ Response interceptedResponse = interceptor.intercept(chain);
+
+ if (interceptedResponse == null) {
+ throw new NullPointerException("application interceptor " + interceptor
+ + " returned null");
+ }
+
+ return interceptedResponse;
}
+
+ // No more interceptors. Do HTTP.
+ return getResponse(request, forWebSocket);
}
}
@@ -254,18 +272,20 @@ public class Call {
}
// Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
- engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null, null);
+ engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);
int followUpCount = 0;
while (true) {
if (canceled) {
- engine.releaseConnection();
+ engine.releaseStreamAllocation();
throw new IOException("Canceled");
}
+ boolean releaseConnection = true;
try {
engine.sendRequest();
engine.readResponse();
+ releaseConnection = false;
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
@@ -273,6 +293,7 @@ public class Call {
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = engine.recover(e);
if (retryEngine != null) {
+ releaseConnection = false;
engine = retryEngine;
continue;
}
@@ -282,12 +303,19 @@ public class Call {
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = engine.recover(e, null);
if (retryEngine != null) {
+ releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e;
+ } finally {
+ // We're throwing an unchecked exception. Release any resources.
+ if (releaseConnection) {
+ StreamAllocation streamAllocation = engine.close();
+ streamAllocation.release();
+ }
}
Response response = engine.getResponse();
@@ -295,22 +323,25 @@ public class Call {
if (followUp == null) {
if (!forWebSocket) {
- engine.releaseConnection();
+ engine.releaseStreamAllocation();
}
return response;
}
+ StreamAllocation streamAllocation = engine.close();
+
if (++followUpCount > MAX_FOLLOW_UPS) {
+ streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (!engine.sameConnection(followUp.httpUrl())) {
- engine.releaseConnection();
+ streamAllocation.release();
+ streamAllocation = null;
}
- Connection connection = engine.close();
request = followUp;
- engine = new HttpEngine(client, request, false, false, forWebSocket, connection, null, null,
+ engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
response);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
index 15a2952..bd3df19 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/CertificatePinner.java
@@ -54,7 +54,7 @@ import static java.util.Collections.unmodifiableSet;
*
* String hostname = "publicobject.com";
* CertificatePinner certificatePinner = new CertificatePinner.Builder()
- * .add(hostname, "sha1/BOGUSPIN")
+ * .add(hostname, "sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=")
* .build();
* OkHttpClient client = new OkHttpClient();
* client.setCertificatePinner(certificatePinner);
@@ -74,7 +74,7 @@ import static java.util.Collections.unmodifiableSet;
* sha1/blhOM3W9V/bVQhsWAcLYwPU6n24=: CN=COMODO RSA Certification Authority
* sha1/T5x9IXmcrQ7YuQxXnxoCmeeQ84c=: CN=AddTrust External CA Root
* Pinned certificates for publicobject.com:
- * sha1/BOGUSPIN
+ * sha1/AAAAAAAAAAAAAAAAAAAAAAAAAAA=
* at com.squareup.okhttp.CertificatePinner.check(CertificatePinner.java)
* at com.squareup.okhttp.Connection.upgradeToTls(Connection.java)
* at com.squareup.okhttp.Connection.connect(Connection.java)
@@ -135,7 +135,7 @@ public final class CertificatePinner {
private final Map<String, Set<ByteString>> hostnameToPins;
private CertificatePinner(Builder builder) {
- hostnameToPins = Util.immutableMap(builder.hostnameToPins);
+ this.hostnameToPins = Util.immutableMap(builder.hostnameToPins);
}
/**
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index 2a3614e..203b510 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -16,500 +16,69 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.ConnectionSpecSelector;
-import com.squareup.okhttp.internal.Platform;
-import com.squareup.okhttp.internal.Util;
-import com.squareup.okhttp.internal.framed.FramedConnection;
-import com.squareup.okhttp.internal.http.FramedTransport;
-import com.squareup.okhttp.internal.http.HttpConnection;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.HttpTransport;
-import com.squareup.okhttp.internal.http.OkHeaders;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
-import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-import java.io.IOException;
-import java.net.Proxy;
import java.net.Socket;
-import java.net.UnknownServiceException;
-import java.security.cert.X509Certificate;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import javax.net.ssl.SSLPeerUnverifiedException;
-import javax.net.ssl.SSLSocket;
-import javax.net.ssl.SSLSocketFactory;
-import okio.BufferedSink;
-import okio.BufferedSource;
-import okio.Source;
-
-import static com.squareup.okhttp.internal.Util.closeQuietly;
-import static java.net.HttpURLConnection.HTTP_OK;
-import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
/**
- * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be
- * used for multiple HTTP request/response exchanges. Connections may be direct
- * to the origin server or via a proxy.
+ * The sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection. May be used for multiple
+ * HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.
*
- * <p>Typically instances of this class are created, connected and exercised
- * automatically by the HTTP client. Applications may use this class to monitor
- * HTTP connections as members of a {@linkplain ConnectionPool connection pool}.
+ * <p>Typically instances of this class are created, connected and exercised automatically by the
+ * HTTP client. Applications may use this class to monitor HTTP connections as members of a
+ * {@linkplain ConnectionPool connection pool}.
*
- * <p>Do not confuse this class with the misnamed {@code HttpURLConnection},
- * which isn't so much a connection as a single request/response exchange.
+ * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, which isn't so much a
+ * connection as a single request/response exchange.
*
* <h3>Modern TLS</h3>
- * There are tradeoffs when selecting which options to include when negotiating
- * a secure connection to a remote host. Newer TLS options are quite useful:
+ * There are tradeoffs when selecting which options to include when negotiating a secure connection
+ * to a remote host. Newer TLS options are quite useful:
* <ul>
- * <li>Server Name Indication (SNI) enables one IP address to negotiate secure
- * connections for multiple domain names.
- * <li>Application Layer Protocol Negotiation (ALPN) enables the HTTPS port
- * (443) to be used for different HTTP and SPDY protocols.
+ * <li>Server Name Indication (SNI) enables one IP address to negotiate secure connections for
+ * multiple domain names.
+ * <li>Application Layer Protocol Negotiation (ALPN) enables the HTTPS port (443) to be used for
+ * different HTTP and SPDY protocols.
* </ul>
- * Unfortunately, older HTTPS servers refuse to connect when such options are
- * presented. Rather than avoiding these options entirely, this class allows a
- * connection to be attempted with modern options and then retried without them
- * should the attempt fail.
+ * Unfortunately, older HTTPS servers refuse to connect when such options are presented. Rather than
+ * avoiding these options entirely, this class allows a connection to be attempted with modern
+ * options and then retried without them should the attempt fail.
+ *
+ * <h3>Connection Reuse</h3>
+ * <p>Each connection can carry a varying number streams, depending on the underlying protocol being
+ * used. HTTP/1.x connections can carry either zero or one streams. HTTP/2 connections can carry any
+ * number of streams, dynamically configured with {@code SETTINGS_MAX_CONCURRENT_STREAMS}. A
+ * connection currently carrying zero streams is an idle stream. We keep it alive because reusing an
+ * existing connection is typically faster than establishing a new one.
+ *
+ * <p>When a single logical call requires multiple streams due to redirects or authorization
+ * challenges, we prefer to use the same physical connection for all streams in the sequence. There
+ * are potential performance and behavior consequences to this preference. To support this feature,
+ * this class separates <i>allocations</i> from <i>streams</i>. An allocation is created by a call,
+ * used for one or more streams, and then released. An allocated connection won't be stolen by
+ * other calls while a redirect or authorization challenge is being handled.
+ *
+ * <p>When the maximum concurrent streams limit is reduced, some allocations will be rescinded.
+ * Attempting to create new streams on these allocations will fail.
+ *
+ * <p>Note that an allocation may be released before its stream is completed. This is intended to
+ * make bookkeeping easier for the caller: releasing the allocation as soon as the terminal stream
+ * has been found. But only complete the stream once its data stream has been exhausted.
*/
-public final class Connection {
- private final ConnectionPool pool;
- private final Route route;
-
- private Socket socket;
- private boolean connected = false;
- private HttpConnection httpConnection;
- private FramedConnection framedConnection;
- private Protocol protocol = Protocol.HTTP_1_1;
- private long idleStartTimeNs;
- private Handshake handshake;
- private int recycleCount;
-
- /**
- * The object that owns this connection. Null if it is shared (for SPDY),
- * belongs to a pool, or has been discarded. Guarded by {@code pool}, which
- * clears the owner when an incoming connection is recycled.
- */
- private Object owner;
-
- public Connection(ConnectionPool pool, Route route) {
- this.pool = pool;
- this.route = route;
- }
-
- Object getOwner() {
- synchronized (pool) {
- return owner;
- }
- }
-
- void setOwner(Object owner) {
- if (isFramed()) return; // Framed connections are shared.
- synchronized (pool) {
- if (this.owner != null) throw new IllegalStateException("Connection already has an owner!");
- this.owner = owner;
- }
- }
-
- /**
- * Attempts to clears the owner of this connection. Returns true if the owner
- * was cleared and the connection can be pooled or reused. This will return
- * false if the connection cannot be pooled or reused, such as if it was
- * closed with {@link #closeIfOwnedBy}.
- */
- boolean clearOwner() {
- synchronized (pool) {
- if (owner == null) {
- // No owner? Don't reuse this connection.
- return false;
- }
-
- owner = null;
- return true;
- }
- }
-
- /**
- * Closes this connection if it is currently owned by {@code owner}. This also
- * strips the ownership of the connection so it cannot be pooled or reused.
- */
- void closeIfOwnedBy(Object owner) throws IOException {
- if (isFramed()) throw new IllegalStateException();
- synchronized (pool) {
- if (this.owner != owner) {
- return; // Wrong owner. Perhaps a late disconnect?
- }
-
- this.owner = null; // Drop the owner so the connection won't be reused.
- }
-
- // Don't close() inside the synchronized block.
- if (socket != null) {
- socket.close();
- }
- }
-
- void connect(int connectTimeout, int readTimeout, int writeTimeout, Request request,
- List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
- if (connected) throw new IllegalStateException("already connected");
-
- RouteException routeException = null;
- ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
- Proxy proxy = route.getProxy();
- Address address = route.getAddress();
-
- if (route.address.getSslSocketFactory() == null
- && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
- throw new RouteException(new UnknownServiceException(
- "CLEARTEXT communication not supported: " + connectionSpecs));
- }
-
- while (!connected) {
- try {
- socket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
- ? address.getSocketFactory().createSocket()
- : new Socket(proxy);
- connectSocket(connectTimeout, readTimeout, writeTimeout, request,
- connectionSpecSelector);
- connected = true; // Success!
- } catch (IOException e) {
- Util.closeQuietly(socket);
- socket = null;
-
- if (routeException == null) {
- routeException = new RouteException(e);
- } else {
- routeException.addConnectException(e);
- }
-
- if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
- throw routeException;
- }
- }
- }
- }
-
- /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
- private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
- Request request, ConnectionSpecSelector connectionSpecSelector) throws IOException {
- socket.setSoTimeout(readTimeout);
- Platform.get().connectSocket(socket, route.getSocketAddress(), connectTimeout);
-
- if (route.address.getSslSocketFactory() != null) {
- connectTls(readTimeout, writeTimeout, request, connectionSpecSelector);
- }
-
- if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
- socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
- framedConnection = new FramedConnection.Builder(route.address.uriHost, true, socket)
- .protocol(protocol).build();
- framedConnection.sendConnectionPreface();
- } else {
- httpConnection = new HttpConnection(pool, this, socket);
- }
- }
-
- private void connectTls(int readTimeout, int writeTimeout, Request request,
- ConnectionSpecSelector connectionSpecSelector) throws IOException {
- if (route.requiresTunnel()) {
- createTunnel(readTimeout, writeTimeout, request);
- }
-
- Address address = route.getAddress();
- SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
- boolean success = false;
- SSLSocket sslSocket = null;
- try {
- // Create the wrapper over the connected socket.
- sslSocket = (SSLSocket) sslSocketFactory.createSocket(
- socket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
-
- // Configure the socket's ciphers, TLS versions, and extensions.
- ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
- if (connectionSpec.supportsTlsExtensions()) {
- Platform.get().configureTlsExtensions(
- sslSocket, address.getUriHost(), address.getProtocols());
- }
-
- // Force handshake. This can throw!
- sslSocket.startHandshake();
- Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
-
- // Verify that the socket's certificates are acceptable for the target host.
- if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
- X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
- throw new SSLPeerUnverifiedException("Hostname " + address.getUriHost() + " not verified:"
- + "\n certificate: " + CertificatePinner.pin(cert)
- + "\n DN: " + cert.getSubjectDN().getName()
- + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
- }
-
- // Check that the certificate pinner is satisfied by the certificates presented.
- address.getCertificatePinner().check(address.getUriHost(),
- unverifiedHandshake.peerCertificates());
-
- // Success! Save the handshake and the ALPN protocol.
- String maybeProtocol = connectionSpec.supportsTlsExtensions()
- ? Platform.get().getSelectedProtocol(sslSocket)
- : null;
- protocol = maybeProtocol != null
- ? Protocol.get(maybeProtocol)
- : Protocol.HTTP_1_1;
- handshake = unverifiedHandshake;
- socket = sslSocket;
- success = true;
- } catch (AssertionError e) {
- if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
- throw e;
- } finally {
- if (sslSocket != null) {
- Platform.get().afterHandshake(sslSocket);
- }
- if (!success) {
- closeQuietly(sslSocket);
- }
- }
- }
-
- /**
- * To make an HTTPS connection over an HTTP proxy, send an unencrypted
- * CONNECT request to create the proxy connection. This may need to be
- * retried if the proxy requires authorization.
- */
- private void createTunnel(int readTimeout, int writeTimeout, Request request) throws IOException {
- // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
- Request tunnelRequest = createTunnelRequest(request);
- HttpConnection tunnelConnection = new HttpConnection(pool, this, socket);
- tunnelConnection.setTimeouts(readTimeout, writeTimeout);
- HttpUrl url = tunnelRequest.httpUrl();
- String requestLine = "CONNECT " + url.host() + ":" + url.port() + " HTTP/1.1";
- while (true) {
- tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
- tunnelConnection.flush();
- Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
- // The response body from a CONNECT should be empty, but if it is not then we should consume
- // it before proceeding.
- long contentLength = OkHeaders.contentLength(response);
- if (contentLength == -1L) {
- contentLength = 0L;
- }
- Source body = tunnelConnection.newFixedLengthSource(contentLength);
- Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
- body.close();
-
- switch (response.code()) {
- case HTTP_OK:
- // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
- // that happens, then we will have buffered bytes that are needed by the SSLSocket!
- // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
- // that it will almost certainly fail because the proxy has sent unexpected data.
- if (tunnelConnection.bufferSize() > 0) {
- throw new IOException("TLS tunnel buffered too many bytes!");
- }
- return;
-
- case HTTP_PROXY_AUTH:
- tunnelRequest = OkHeaders.processAuthHeader(
- route.getAddress().getAuthenticator(), response, route.getProxy());
- if (tunnelRequest != null) continue;
- throw new IOException("Failed to authenticate with proxy");
-
- default:
- throw new IOException(
- "Unexpected response code for CONNECT: " + response.code());
- }
- }
- }
-
- /**
- * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
- * no tunnel is necessary. Everything in the tunnel request is sent
- * unencrypted to the proxy server, so tunnels include only the minimum set of
- * headers. This avoids sending potentially sensitive data like HTTP cookies
- * to the proxy unencrypted.
- */
- private Request createTunnelRequest(Request request) throws IOException {
- HttpUrl tunnelUrl = new HttpUrl.Builder()
- .scheme("https")
- .host(request.httpUrl().host())
- .port(request.httpUrl().port())
- .build();
- Request.Builder result = new Request.Builder()
- .url(tunnelUrl)
- .header("Host", Util.hostHeader(tunnelUrl))
- .header("Proxy-Connection", "Keep-Alive"); // For HTTP/1.0 proxies like Squid.
-
- // Copy over the User-Agent header if it exists.
- String userAgent = request.header("User-Agent");
- if (userAgent != null) {
- result.header("User-Agent", userAgent);
- }
-
- // Copy over the Proxy-Authorization header if it exists.
- String proxyAuthorization = request.header("Proxy-Authorization");
- if (proxyAuthorization != null) {
- result.header("Proxy-Authorization", proxyAuthorization);
- }
-
- return result.build();
- }
-
- /**
- * Connects this connection if it isn't already. This creates tunnels, shares
- * the connection with the connection pool, and configures timeouts.
- */
- void connectAndSetOwner(OkHttpClient client, Object owner, Request request)
- throws RouteException {
- setOwner(owner);
-
- if (!isConnected()) {
- List<ConnectionSpec> connectionSpecs = route.address.getConnectionSpecs();
- connect(client.getConnectTimeout(), client.getReadTimeout(), client.getWriteTimeout(),
- request, connectionSpecs, client.getRetryOnConnectionFailure());
- if (isFramed()) {
- client.getConnectionPool().share(this);
- }
- client.routeDatabase().connected(getRoute());
- }
-
- setTimeouts(client.getReadTimeout(), client.getWriteTimeout());
- }
-
- /** Returns true if {@link #connect} has been attempted on this connection. */
- boolean isConnected() {
- return connected;
- }
-
+public interface Connection {
/** Returns the route used by this connection. */
- public Route getRoute() {
- return route;
- }
+ Route getRoute();
/**
* Returns the socket that this connection uses, or null if the connection
* is not currently connected.
*/
- public Socket getSocket() {
- return socket;
- }
-
- BufferedSource rawSource() {
- if (httpConnection == null) throw new UnsupportedOperationException();
- return httpConnection.rawSource();
- }
-
- BufferedSink rawSink() {
- if (httpConnection == null) throw new UnsupportedOperationException();
- return httpConnection.rawSink();
- }
-
- /** Returns true if this connection is alive. */
- boolean isAlive() {
- return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
- }
-
- /**
- * Returns true if we are confident that we can read data from this
- * connection. This is more expensive and more accurate than {@link
- * #isAlive()}; callers should check {@link #isAlive()} first.
- */
- boolean isReadable() {
- if (httpConnection != null) return httpConnection.isReadable();
- return true; // Framed connections, and connections before connect() are both optimistic.
- }
-
- void resetIdleStartTime() {
- if (framedConnection != null) throw new IllegalStateException("framedConnection != null");
- this.idleStartTimeNs = System.nanoTime();
- }
-
- /** Returns true if this connection is idle. */
- boolean isIdle() {
- return framedConnection == null || framedConnection.isIdle();
- }
-
- /**
- * Returns the time in ns when this connection became idle. Undefined if
- * this connection is not idle.
- */
- long getIdleStartTimeNs() {
- return framedConnection == null ? idleStartTimeNs : framedConnection.getIdleStartTimeNs();
- }
-
- public Handshake getHandshake() {
- return handshake;
- }
-
- /** Returns the transport appropriate for this connection. */
- Transport newTransport(HttpEngine httpEngine) throws IOException {
- return (framedConnection != null)
- ? new FramedTransport(httpEngine, framedConnection)
- : new HttpTransport(httpEngine, httpConnection);
- }
-
- /**
- * Returns true if this is a SPDY connection. Such connections can be used
- * in multiple HTTP requests simultaneously.
- */
- boolean isFramed() {
- return framedConnection != null;
- }
-
- /**
- * Returns the protocol negotiated by this connection, or {@link
- * Protocol#HTTP_1_1} if no protocol has been negotiated.
- */
- public Protocol getProtocol() {
- return protocol;
- }
-
- /**
- * Sets the protocol negotiated by this connection. Typically this is used
- * when an HTTP/1.1 request is sent and an HTTP/1.0 response is received.
- */
- void setProtocol(Protocol protocol) {
- if (protocol == null) throw new IllegalArgumentException("protocol == null");
- this.protocol = protocol;
- }
-
- void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis)
- throws RouteException {
- if (!connected) throw new IllegalStateException("setTimeouts - not connected");
+ Socket getSocket();
- // Don't set timeouts on shared SPDY connections.
- if (httpConnection != null) {
- try {
- socket.setSoTimeout(readTimeoutMillis);
- } catch (IOException e) {
- throw new RouteException(e);
- }
- httpConnection.setTimeouts(readTimeoutMillis, writeTimeoutMillis);
- }
- }
-
- void incrementRecycleCount() {
- recycleCount++;
- }
+ Handshake getHandshake();
/**
- * Returns the number of times this connection has been returned to the
- * connection pool.
+ * Returns the protocol negotiated by this connection, or {@link Protocol#HTTP_1_1} if no protocol
+ * has been negotiated. This method returns {@link Protocol#HTTP_1_1} even if the remote peer is
+ * using {@link Protocol#HTTP_1_0}.
*/
- int recycleCount() {
- return recycleCount;
- }
-
- @Override public String toString() {
- return "Connection{"
- + route.address.uriHost + ":" + route.address.uriPort
- + ", proxy="
- + route.proxy
- + " hostAddress="
- + route.inetSocketAddress.getAddress().getHostAddress()
- + " cipherSuite="
- + (handshake != null ? handshake.cipherSuite() : "none")
- + " protocol="
- + protocol
- + '}';
- }
+ Protocol getProtocol();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
index da3ac73..6f8efc4 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionPool.java
@@ -16,13 +16,17 @@
*/
package com.squareup.okhttp;
-import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RouteDatabase;
import com.squareup.okhttp.internal.Util;
-import java.net.SocketException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
+import java.lang.ref.Reference;
+import java.util.ArrayDeque;
import java.util.ArrayList;
-import java.util.LinkedList;
+import java.util.Deque;
+import java.util.Iterator;
import java.util.List;
-import java.util.ListIterator;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
@@ -31,8 +35,8 @@ import java.util.concurrent.TimeUnit;
/**
* Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP
* requests that share the same {@link com.squareup.okhttp.Address} may share a
- * {@link com.squareup.okhttp.Connection}. This class implements the policy of
- * which connections to keep open for future use.
+ * {@link Connection}. This class implements the policy of which connections to
+ * keep open for future use.
*
* <p>The {@link #getDefault() system-wide default} uses system properties for
* tuning parameters:
@@ -60,7 +64,8 @@ public final class ConnectionPool {
String keepAlive = System.getProperty("http.keepAlive");
String keepAliveDuration = System.getProperty("http.keepAliveDuration");
String maxIdleConnections = System.getProperty("http.maxConnections");
- long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration)
+ long keepAliveDurationMs = keepAliveDuration != null
+ ? Long.parseLong(keepAliveDuration)
: DEFAULT_KEEP_ALIVE_DURATION_MS;
if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) {
systemDefault = new ConnectionPool(0, keepAliveDurationMs);
@@ -71,43 +76,73 @@ public final class ConnectionPool {
}
}
- /** The maximum number of idle connections for each address. */
- private final int maxIdleConnections;
- private final long keepAliveDurationNs;
-
- private final LinkedList<Connection> connections = new LinkedList<>();
-
/**
* A background thread is used to cleanup expired connections. There will be, at most, a single
- * thread running per connection pool.
- *
- * <p>A {@link ThreadPoolExecutor} is used and not a
- * {@link java.util.concurrent.ScheduledThreadPoolExecutor}; ScheduledThreadPoolExecutors do not
- * shrink. This executor shrinks the thread pool after a period of inactivity, and starts threads
- * as needed. Delays are instead handled by the {@link #connectionsCleanupRunnable}. It is
- * important that the {@link #connectionsCleanupRunnable} stops eventually, otherwise it will pin
- * the thread, and thus the connection pool, in memory.
+ * thread running per connection pool. We use a thread pool executor because it can shrink to
+ * zero threads, permitting this pool to be garbage collected.
*/
- private Executor executor = new ThreadPoolExecutor(
+ private final Executor executor = new ThreadPoolExecutor(
0 /* corePoolSize */, 1 /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
- private final Runnable connectionsCleanupRunnable = new Runnable() {
+ /** The maximum number of idle connections for each address. */
+ private final int maxIdleConnections;
+ private final long keepAliveDurationNs;
+ private Runnable cleanupRunnable = new Runnable() {
@Override public void run() {
- runCleanupUntilPoolIsEmpty();
+ while (true) {
+ long waitNanos = cleanup(System.nanoTime());
+ if (waitNanos == -1) return;
+ if (waitNanos > 0) {
+ long waitMillis = waitNanos / 1000000L;
+ waitNanos -= (waitMillis * 1000000L);
+ synchronized (ConnectionPool.this) {
+ try {
+ ConnectionPool.this.wait(waitMillis, (int) waitNanos);
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+ }
}
};
+ private final Deque<RealConnection> connections = new ArrayDeque<>();
+ final RouteDatabase routeDatabase = new RouteDatabase();
+
public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) {
+ this(maxIdleConnections, keepAliveDurationMs, TimeUnit.MILLISECONDS);
+ }
+
+ public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.maxIdleConnections = maxIdleConnections;
- this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000;
+ this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);
+
+ // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
+ if (keepAliveDuration <= 0) {
+ throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
+ }
}
public static ConnectionPool getDefault() {
return systemDefault;
}
- /** Returns total number of connections in the pool. */
+ /** Returns the number of idle connections in the pool. */
+ public synchronized int getIdleConnectionCount() {
+ int total = 0;
+ for (RealConnection connection : connections) {
+ if (connection.allocations.isEmpty()) total++;
+ }
+ return total;
+ }
+
+ /**
+ * Returns total number of connections in the pool. Note that prior to OkHttp 2.7 this included
+ * only idle connections and SPDY connections. In OkHttp 2.7 this includes all connections, both
+ * active and inactive. Use {@link #getIdleConnectionCount()} to count connections not currently
+ * in use.
+ */
public synchronized int getConnectionCount() {
return connections.size();
}
@@ -121,8 +156,8 @@ public final class ConnectionPool {
/** Returns total number of multiplexed connections in the pool. */
public synchronized int getMultiplexedConnectionCount() {
int total = 0;
- for (Connection connection : connections) {
- if (connection.isFramed()) total++;
+ for (RealConnection connection : connections) {
+ if (connection.isMultiplexed()) total++;
}
return total;
}
@@ -133,205 +168,156 @@ public final class ConnectionPool {
}
/** Returns a recycled connection to {@code address}, or null if no such connection exists. */
- public synchronized Connection get(Address address) {
- Connection foundConnection = null;
- for (ListIterator<Connection> i = connections.listIterator(connections.size());
- i.hasPrevious(); ) {
- Connection connection = i.previous();
- if (!connection.getRoute().getAddress().equals(address)
- || !connection.isAlive()
- || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) {
- continue;
- }
- i.remove();
- if (!connection.isFramed()) {
- try {
- Platform.get().tagSocket(connection.getSocket());
- } catch (SocketException e) {
- Util.closeQuietly(connection.getSocket());
- // When unable to tag, skip recycling and close
- Platform.get().logW("Unable to tagSocket(): " + e);
- continue;
- }
+ RealConnection get(Address address, StreamAllocation streamAllocation) {
+ assert (Thread.holdsLock(this));
+ for (RealConnection connection : connections) {
+ // TODO(jwilson): this is awkward. We're already holding a lock on 'this', and
+ // connection.allocationLimit() may also lock the FramedConnection.
+ if (connection.allocations.size() < connection.allocationLimit()
+ && address.equals(connection.getRoute().address)
+ && !connection.noNewStreams) {
+ streamAllocation.acquire(connection);
+ return connection;
}
- foundConnection = connection;
- break;
- }
-
- if (foundConnection != null && foundConnection.isFramed()) {
- connections.addFirst(foundConnection); // Add it back after iteration.
- }
-
- return foundConnection;
- }
-
- /**
- * Gives {@code connection} to the pool. The pool may store the connection,
- * or close it, as its policy describes.
- *
- * <p>It is an error to use {@code connection} after calling this method.
- */
- void recycle(Connection connection) {
- if (connection.isFramed()) {
- return;
- }
-
- if (!connection.clearOwner()) {
- return; // This connection isn't eligible for reuse.
- }
-
- if (!connection.isAlive()) {
- Util.closeQuietly(connection.getSocket());
- return;
- }
-
- try {
- Platform.get().untagSocket(connection.getSocket());
- } catch (SocketException e) {
- // When unable to remove tagging, skip recycling and close.
- Platform.get().logW("Unable to untagSocket(): " + e);
- Util.closeQuietly(connection.getSocket());
- return;
- }
-
- synchronized (this) {
- addConnection(connection);
- connection.incrementRecycleCount();
- connection.resetIdleStartTime();
}
+ return null;
}
- private void addConnection(Connection connection) {
- boolean empty = connections.isEmpty();
- connections.addFirst(connection);
- if (empty) {
- executor.execute(connectionsCleanupRunnable);
- } else {
- notifyAll();
+ void put(RealConnection connection) {
+ assert (Thread.holdsLock(this));
+ if (connections.isEmpty()) {
+ executor.execute(cleanupRunnable);
}
+ connections.add(connection);
}
/**
- * Shares the SPDY connection with the pool. Callers to this method may
- * continue to use {@code connection}.
+ * Notify this pool that {@code connection} has become idle. Returns true if the connection
+ * has been removed from the pool and should be closed.
*/
- void share(Connection connection) {
- if (!connection.isFramed()) throw new IllegalArgumentException();
- if (!connection.isAlive()) return;
- synchronized (this) {
- addConnection(connection);
+ boolean connectionBecameIdle(RealConnection connection) {
+ assert (Thread.holdsLock(this));
+ if (connection.noNewStreams || maxIdleConnections == 0) {
+ connections.remove(connection);
+ return true;
+ } else {
+ notifyAll(); // Awake the cleanup thread: we may have exceeded the idle connection limit.
+ return false;
}
}
- /** Close and remove all connections in the pool. */
+ /** Close and remove all idle connections in the pool. */
public void evictAll() {
- List<Connection> toEvict;
+ List<RealConnection> evictedConnections = new ArrayList<>();
synchronized (this) {
- toEvict = new ArrayList<>(connections);
- connections.clear();
- notifyAll();
- }
-
- for (int i = 0, size = toEvict.size(); i < size; i++) {
- Util.closeQuietly(toEvict.get(i).getSocket());
+ for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
+ RealConnection connection = i.next();
+ if (connection.allocations.isEmpty()) {
+ connection.noNewStreams = true;
+ evictedConnections.add(connection);
+ i.remove();
+ }
+ }
}
- }
- private void runCleanupUntilPoolIsEmpty() {
- while (true) {
- if (!performCleanup()) return; // Halt cleanup.
+ for (RealConnection connection : evictedConnections) {
+ Util.closeQuietly(connection.getSocket());
}
}
/**
- * 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.)
+ * Performs maintenance on this pool, evicting the connection that has been idle the longest if
+ * either it has exceeded the keep alive limit or the idle connections limit.
*
- * <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.
+ * <p>Returns the duration in nanos to sleep until the next scheduled call to this method.
+ * Returns -1 if no further cleanups are required.
*/
- // VisibleForTesting
- boolean performCleanup() {
- List<Connection> evictableConnections;
+ long cleanup(long now) {
+ int inUseConnectionCount = 0;
+ int idleConnectionCount = 0;
+ RealConnection longestIdleConnection = null;
+ long longestIdleDurationNs = Long.MIN_VALUE;
+ // Find either a connection to evict, or the time that the next eviction is due.
synchronized (this) {
- 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);
+ for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
+ RealConnection connection = i.next();
+
+ // If the connection is in use, keep searching.
+ if (pruneAndGetAllocationCount(connection, now) > 0) {
+ inUseConnectionCount++;
+ continue;
}
- }
- // 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;
+ idleConnectionCount++;
+
+ // If the connection is ready to be evicted, we're done.
+ long idleDurationNs = now - connection.idleAtNanos;
+ if (idleDurationNs > longestIdleDurationNs) {
+ longestIdleDurationNs = idleDurationNs;
+ longestIdleConnection = connection;
}
}
- // 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) {
- }
+ if (longestIdleDurationNs >= this.keepAliveDurationNs
+ || idleConnectionCount > this.maxIdleConnections) {
+ // We've found a connection to evict. Remove it from the list, then close it below (outside
+ // of the synchronized block).
+ connections.remove(longestIdleConnection);
+
+ } else if (idleConnectionCount > 0) {
+ // A connection will be ready to evict soon.
+ return keepAliveDurationNs - longestIdleDurationNs;
+
+ } else if (inUseConnectionCount > 0) {
+ // All connections are in use. It'll be at least the keep alive duration 'til we run again.
+ return keepAliveDurationNs;
+
+ } else {
+ // No connections, idle or in use.
+ return -1;
}
}
- // Actually do the eviction. Note that we avoid synchronized() when closing sockets.
- for (int i = 0, size = evictableConnections.size(); i < size; i++) {
- Connection expiredConnection = evictableConnections.get(i);
- Util.closeQuietly(expiredConnection.getSocket());
- }
+ Util.closeQuietly(longestIdleConnection.getSocket());
- return true; // Cleanup continues.
+ // Cleanup again immediately.
+ return 0;
}
/**
- * Replace the default {@link Executor} with a different one. Only use in tests.
+ * Prunes any leaked allocations and then returns the number of remaining live allocations on
+ * {@code connection}. Allocations are leaked if the connection is tracking them but the
+ * application code has abandoned them. Leak detection is imprecise and relies on garbage
+ * collection.
*/
- // VisibleForTesting
- void replaceCleanupExecutorForTests(Executor cleanupExecutor) {
- this.executor = cleanupExecutor;
+ private int pruneAndGetAllocationCount(RealConnection connection, long now) {
+ List<Reference<StreamAllocation>> references = connection.allocations;
+ for (int i = 0; i < references.size(); ) {
+ Reference<StreamAllocation> reference = references.get(i);
+
+ if (reference.get() != null) {
+ i++;
+ continue;
+ }
+
+ // We've discovered a leaked allocation. This is an application bug.
+ Internal.logger.warning("A connection to " + connection.getRoute().getAddress().url()
+ + " was leaked. Did you forget to close a response body?");
+ references.remove(i);
+ connection.noNewStreams = true;
+
+ // If this was the last allocation, the connection is eligible for immediate eviction.
+ if (references.isEmpty()) {
+ connection.idleAtNanos = now - keepAliveDurationNs;
+ return 0;
+ }
+ }
+
+ return references.size();
}
- /**
- * Returns a snapshot of the connections in this pool, ordered from newest to
- * oldest. Only use in tests.
- */
- // VisibleForTesting
- synchronized List<Connection> getConnections() {
- return new ArrayList<>(connections);
+ void setCleanupRunnableForTest(Runnable cleanupRunnable) {
+ this.cleanupRunnable = cleanupRunnable;
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
index 5e0f7d8..af63afd 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/ConnectionSpec.java
@@ -20,14 +20,24 @@ import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLSocket;
+import static com.squareup.okhttp.internal.Util.concat;
+import static com.squareup.okhttp.internal.Util.contains;
+
/**
* Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
* https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
* connection.
+ *
+ * <p>The TLS versions configured in a connection spec are only be used if they are also enabled in
+ * the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used
+ * even if it is present on the connection spec. The same policy also applies to cipher suites.
+ *
+ * <p>Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to
+ * defer all feature selection to the underlying SSL socket.
*/
public final class ConnectionSpec {
- // This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5.
+ // This is a subset of the cipher suites supported in Chrome 46, current as of 2015-11-05.
// All of these suites are available on Android 5.0; earlier releases support a subset of
// these suites. https://github.com/square/okhttp/issues/330
private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
@@ -43,7 +53,6 @@ public final class ConnectionSpec {
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
- CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
@@ -67,19 +76,11 @@ public final class ConnectionSpec {
/** Unencrypted, unauthenticated connections for {@code http:} URLs. */
public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
- final boolean tls;
-
- /**
- * Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use
- * default set".
- */
+ private final boolean tls;
+ private final boolean supportsTlsExtensions;
private final String[] cipherSuites;
-
- /** Used if tls == true. The TLS protocol versions to use. */
private final String[] tlsVersions;
- final boolean supportsTlsExtensions;
-
private ConnectionSpec(Builder builder) {
this.tls = builder.tls;
this.cipherSuites = builder.cipherSuites;
@@ -92,13 +93,12 @@ public final class ConnectionSpec {
}
/**
- * Returns the cipher suites to use for a connection. This method can return {@code null} if the
- * cipher suites enabled by default should be used.
+ * Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL
+ * socket's enabled cipher suites should be used.
*/
public List<CipherSuite> cipherSuites() {
- if (cipherSuites == null) {
- return null;
- }
+ if (cipherSuites == null) return null;
+
CipherSuite[] result = new CipherSuite[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
result[i] = CipherSuite.forJavaName(cipherSuites[i]);
@@ -106,7 +106,13 @@ public final class ConnectionSpec {
return Util.immutableList(result);
}
+ /**
+ * Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of
+ * the SSL socket's enabled TLS versions should be used.
+ */
public List<TlsVersion> tlsVersions() {
+ if (tlsVersions == null) return null;
+
TlsVersion[] result = new TlsVersion[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
result[i] = TlsVersion.forJavaName(tlsVersions[i]);
@@ -122,57 +128,40 @@ public final class ConnectionSpec {
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
- sslSocket.setEnabledProtocols(specToApply.tlsVersions);
-
- String[] cipherSuitesToEnable = specToApply.cipherSuites;
- // null means "use default set".
- if (cipherSuitesToEnable != null) {
- sslSocket.setEnabledCipherSuites(cipherSuitesToEnable);
+ if (specToApply.tlsVersions != null) {
+ sslSocket.setEnabledProtocols(specToApply.tlsVersions);
+ }
+ if (specToApply.cipherSuites != null) {
+ sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
/**
- * Returns a copy of this that omits cipher suites and TLS versions not enabled by
- * {@code sslSocket}.
+ * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
+ * sslSocket}.
*/
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
- String[] cipherSuitesToEnable = null;
- if (cipherSuites != null) {
- String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites();
- cipherSuitesToEnable =
- Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom);
+ String[] cipherSuitesIntersection = cipherSuites != null
+ ? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
+ : sslSocket.getEnabledCipherSuites();
+ String[] tlsVersionsIntersection = tlsVersions != null
+ ? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
+ : sslSocket.getEnabledProtocols();
+
+ // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
+ // the SCSV cipher is added to signal that a protocol fallback has taken place.
+ if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) {
+ cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
}
- if (isFallback) {
- // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
- // the SCSV cipher is added to signal that a protocol fallback has taken place.
- final String fallbackScsv = "TLS_FALLBACK_SCSV";
- boolean socketSupportsFallbackScsv =
- Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv);
-
- if (socketSupportsFallbackScsv) {
- // Add the SCSV cipher to the set of enabled cipher suites iff it is supported.
- String[] oldEnabledCipherSuites = cipherSuitesToEnable != null
- ? cipherSuitesToEnable
- : sslSocket.getEnabledCipherSuites();
- String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1];
- System.arraycopy(oldEnabledCipherSuites, 0,
- newEnabledCipherSuites, 0, oldEnabledCipherSuites.length);
- newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
- cipherSuitesToEnable = newEnabledCipherSuites;
- }
- }
-
- String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols();
- String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom);
return new Builder(this)
- .cipherSuites(cipherSuitesToEnable)
- .tlsVersions(protocolsToEnable)
+ .cipherSuites(cipherSuitesIntersection)
+ .tlsVersions(tlsVersionsIntersection)
.build();
}
/**
- * Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec.
+ * Returns {@code true} if the socket, as currently configured, supports this connection spec.
* In order for a socket to be compatible the enabled cipher suites and protocols must intersect.
*
* <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
@@ -187,20 +176,17 @@ public final class ConnectionSpec {
return false;
}
- String[] enabledProtocols = socket.getEnabledProtocols();
- boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols);
- if (!requiredProtocolsEnabled) {
+ if (tlsVersions != null
+ && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
return false;
}
- boolean requiredCiphersEnabled;
- if (cipherSuites == null) {
- requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0;
- } else {
- String[] enabledCipherSuites = socket.getEnabledCipherSuites();
- requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites);
+ if (cipherSuites != null
+ && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
+ return false;
}
- return requiredCiphersEnabled;
+
+ return true;
}
/**
@@ -220,15 +206,6 @@ public final class ConnectionSpec {
return false;
}
- private static <T> boolean contains(T[] array, T value) {
- for (T arrayValue : array) {
- if (Util.equal(value, arrayValue)) {
- return true;
- }
- }
- return false;
- }
-
@Override public boolean equals(Object other) {
if (!(other instanceof ConnectionSpec)) return false;
if (other == this) return true;
@@ -256,16 +233,17 @@ public final class ConnectionSpec {
}
@Override public String toString() {
- if (tls) {
- List<CipherSuite> cipherSuites = cipherSuites();
- String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString();
- return "ConnectionSpec(cipherSuites=" + cipherSuitesString
- + ", tlsVersions=" + tlsVersions()
- + ", supportsTlsExtensions=" + supportsTlsExtensions
- + ")";
- } else {
+ if (!tls) {
return "ConnectionSpec()";
}
+
+ String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]";
+ String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]";
+ return "ConnectionSpec("
+ + "cipherSuites=" + cipherSuitesString
+ + ", tlsVersions=" + tlsVersionsString
+ + ", supportsTlsExtensions=" + supportsTlsExtensions
+ + ")";
}
public static final class Builder {
@@ -285,56 +263,58 @@ public final class ConnectionSpec {
this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
}
+ public Builder allEnabledCipherSuites() {
+ if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
+ this.cipherSuites = null;
+ return this;
+ }
+
public Builder cipherSuites(CipherSuite... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
- // Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
strings[i] = cipherSuites[i].javaName;
}
- this.cipherSuites = strings;
- return this;
+ return cipherSuites(strings);
}
public Builder cipherSuites(String... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
- if (cipherSuites == null) {
- this.cipherSuites = null;
- } else {
- // This makes a defensive copy!
- this.cipherSuites = cipherSuites.clone();
+ if (cipherSuites.length == 0) {
+ throw new IllegalArgumentException("At least one cipher suite is required");
}
+ this.cipherSuites = cipherSuites.clone(); // Defensive copy.
+ return this;
+ }
+
+ public Builder allEnabledTlsVersions() {
+ if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
+ this.tlsVersions = null;
return this;
}
public Builder tlsVersions(TlsVersion... tlsVersions) {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
- if (tlsVersions.length == 0) {
- throw new IllegalArgumentException("At least one TlsVersion is required");
- }
- // Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
strings[i] = tlsVersions[i].javaName;
}
- this.tlsVersions = strings;
- return this;
+
+ return tlsVersions(strings);
}
public Builder tlsVersions(String... tlsVersions) {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
- if (tlsVersions == null) {
- this.tlsVersions = null;
- } else {
- // This makes a defensive copy!
- this.tlsVersions = tlsVersions.clone();
+ if (tlsVersions.length == 0) {
+ throw new IllegalArgumentException("At least one TLS version is required");
}
+ this.tlsVersions = tlsVersions.clone(); // Defensive copy.
return this;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
index a934670..a669b94 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dispatcher.java
@@ -125,7 +125,7 @@ public final class Dispatcher {
if (Util.equal(tag, call.tag())) {
call.get().canceled = true;
HttpEngine engine = call.get().engine;
- if (engine != null) engine.disconnect();
+ if (engine != null) engine.cancel();
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Dns.java b/okhttp/src/main/java/com/squareup/okhttp/Dns.java
new file mode 100644
index 0000000..1ebd392
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/Dns.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2012 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A domain name service that resolves IP addresses for host names. Most applications will use the
+ * {@linkplain #SYSTEM system DNS service}, which is the default. Some applications may provide
+ * their own implementation to use a different DNS server, to prefer IPv6 addresses, to prefer IPv4
+ * addresses, or to force a specific known IP address.
+ *
+ * <p>Implementations of this interface must be safe for concurrent use.
+ */
+public interface Dns {
+ /**
+ * A DNS that uses {@link InetAddress#getAllByName} to ask the underlying operating system to
+ * lookup IP addresses. Most custom {@link Dns} implementations should delegate to this instance.
+ */
+ Dns SYSTEM = new Dns() {
+ @Override public List<InetAddress> lookup(String hostname) throws UnknownHostException {
+ if (hostname == null) throw new UnknownHostException("hostname == null");
+ return Arrays.asList(InetAddress.getAllByName(hostname));
+ }
+ };
+
+ /**
+ * Returns the IP addresses of {@code hostname}, in the order they will be attempted by OkHttp.
+ * If a connection to an address fails, OkHttp will retry the connection with the next address
+ * until either a connection is made, the set of IP addresses is exhausted, or a limit is
+ * exceeded.
+ */
+ List<InetAddress> lookup(String hostname) throws UnknownHostException;
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
index 96f6917..f5134d9 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/FormEncodingBuilder.java
@@ -33,10 +33,10 @@ public final class FormEncodingBuilder {
content.writeByte('&');
}
HttpUrl.canonicalize(content, name, 0, name.length(),
- HttpUrl.FORM_ENCODE_SET, false, false, true, true);
+ HttpUrl.FORM_ENCODE_SET, false, true, true);
content.writeByte('=');
HttpUrl.canonicalize(content, value, 0, value.length(),
- HttpUrl.FORM_ENCODE_SET, false, false, true, true);
+ HttpUrl.FORM_ENCODE_SET, false, true, true);
return this;
}
@@ -46,10 +46,10 @@ public final class FormEncodingBuilder {
content.writeByte('&');
}
HttpUrl.canonicalize(content, name, 0, name.length(),
- HttpUrl.FORM_ENCODE_SET, true, false, true, true);
+ HttpUrl.FORM_ENCODE_SET, true, true, true);
content.writeByte('=');
HttpUrl.canonicalize(content, value, 0, value.length(),
- HttpUrl.FORM_ENCODE_SET, true, false, true, true);
+ HttpUrl.FORM_ENCODE_SET, true, true, true);
return this;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Headers.java b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
index d5b4cef..0dca428 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Headers.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Headers.java
@@ -289,27 +289,9 @@ public final class Headers {
}
}
if (value == null) throw new IllegalArgumentException("value == null");
-
- // Workaround for applications that set trailing "\r", "\n" or "\r\n" on header values.
- // http://b/26422335, http://b/26889631 Android used to allow anything except '\0'.
- int valueLen = value.length();
- if (valueLen >= 2 && value.charAt(valueLen - 2) == '\r' && value.charAt(valueLen - 1) == '\n') {
- value = value.substring(0, value.length() - 2);
- } else if (valueLen > 0
- && (value.charAt(valueLen - 1) == '\n'
- || value.charAt(valueLen - 1) == '\r')) {
- value = value.substring(0, valueLen - 1);
- }
- // End of workaround.
-
for (int i = 0, length = value.length(); i < length; i++) {
char c = value.charAt(i);
- // ANDROID-BEGIN
- // http://b/28867041 - keep things working for apps that rely on Android's (out of spec)
- // UTF-8 header encoding behavior.
- // if (c <= '\u001f' || c >= '\u007f') {
- if (c <= '\u001f' || c == '\u007f') {
- // ANDROID-END
+ if (c <= '\u001f' || c >= '\u007f') {
throw new IllegalArgumentException(String.format(
"Unexpected char %#04x at %d in header value: %s", (int) c, i, value));
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
index 69588ed..b053356 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/HttpUrl.java
@@ -327,29 +327,19 @@ public final class HttpUrl {
}
/**
- * Returns this URL as a {@link URI java.net.URI}. Because {@code URI} is more strict than this
- * class, the returned URI may be semantically different from this URL:
- * <ul>
- * <li>Characters forbidden by URI like {@code [} and {@code |} will be escaped.
- * <li>Invalid percent-encoded sequences like {@code %xx} will be encoded like {@code %25xx}.
- * <li>Whitespace and control characters in the fragment will be stripped.
- * </ul>
+ * Returns this URL as a {@link URI java.net.URI}. Because {@code URI} forbids certain characters
+ * like {@code [} and {@code |}, the returned URI may escape more characters than this URL.
*
- * <p>These differences may have a significant consequence when the URI is interpretted by a
- * webserver. For this reason the {@linkplain URI URI class} and this method should be avoided.
+ * <p>This method throws an unchecked {@link IllegalStateException} if it cannot be converted to a
+ * URI even after escaping forbidden characters. In particular, URLs that contain malformed
+ * percent escapes like {@code http://host/%xx} will trigger this exception.
*/
public URI uri() {
- String uri = newBuilder().reencodeForUri().toString();
try {
+ String uri = newBuilder().reencodeForUri().toString();
return new URI(uri);
} catch (URISyntaxException e) {
- // Unlikely edge case: the URI has a forbidden character in the fragment. Strip it & retry.
- try {
- String stripped = uri.replaceAll("[\\u0000-\\u001F\\u007F-\\u009F\\p{javaWhitespace}]", "");
- return URI.create(stripped);
- } catch (Exception e1) {
- throw new RuntimeException(e); // Unexpected!
- }
+ throw new IllegalStateException("not valid as a java.net.URI: " + url);
}
}
@@ -683,27 +673,25 @@ public final class HttpUrl {
public Builder username(String username) {
if (username == null) throw new IllegalArgumentException("username == null");
- this.encodedUsername = canonicalize(username, USERNAME_ENCODE_SET, false, false, false, true);
+ this.encodedUsername = canonicalize(username, USERNAME_ENCODE_SET, false, false, true);
return this;
}
public Builder encodedUsername(String encodedUsername) {
if (encodedUsername == null) throw new IllegalArgumentException("encodedUsername == null");
- this.encodedUsername = canonicalize(
- encodedUsername, USERNAME_ENCODE_SET, true, false, false, true);
+ this.encodedUsername = canonicalize(encodedUsername, USERNAME_ENCODE_SET, true, false, true);
return this;
}
public Builder password(String password) {
if (password == null) throw new IllegalArgumentException("password == null");
- this.encodedPassword = canonicalize(password, PASSWORD_ENCODE_SET, false, false, false, true);
+ this.encodedPassword = canonicalize(password, PASSWORD_ENCODE_SET, false, false, true);
return this;
}
public Builder encodedPassword(String encodedPassword) {
if (encodedPassword == null) throw new IllegalArgumentException("encodedPassword == null");
- this.encodedPassword = canonicalize(
- encodedPassword, PASSWORD_ENCODE_SET, true, false, false, true);
+ this.encodedPassword = canonicalize(encodedPassword, PASSWORD_ENCODE_SET, true, false, true);
return this;
}
@@ -746,7 +734,7 @@ public final class HttpUrl {
public Builder setPathSegment(int index, String pathSegment) {
if (pathSegment == null) throw new IllegalArgumentException("pathSegment == null");
String canonicalPathSegment = canonicalize(
- pathSegment, 0, pathSegment.length(), PATH_SEGMENT_ENCODE_SET, false, false, false, true);
+ pathSegment, 0, pathSegment.length(), PATH_SEGMENT_ENCODE_SET, false, false, true);
if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) {
throw new IllegalArgumentException("unexpected path segment: " + pathSegment);
}
@@ -759,7 +747,7 @@ public final class HttpUrl {
throw new IllegalArgumentException("encodedPathSegment == null");
}
String canonicalPathSegment = canonicalize(encodedPathSegment,
- 0, encodedPathSegment.length(), PATH_SEGMENT_ENCODE_SET, true, false, false, true);
+ 0, encodedPathSegment.length(), PATH_SEGMENT_ENCODE_SET, true, false, true);
encodedPathSegments.set(index, canonicalPathSegment);
if (isDot(canonicalPathSegment) || isDotDot(canonicalPathSegment)) {
throw new IllegalArgumentException("unexpected path segment: " + encodedPathSegment);
@@ -786,8 +774,7 @@ public final class HttpUrl {
public Builder query(String query) {
this.encodedQueryNamesAndValues = query != null
- ? queryStringToNamesAndValues(canonicalize(
- query, QUERY_ENCODE_SET, false, false, true, true))
+ ? queryStringToNamesAndValues(canonicalize(query, QUERY_ENCODE_SET, false, true, true))
: null;
return this;
}
@@ -795,7 +782,7 @@ public final class HttpUrl {
public Builder encodedQuery(String encodedQuery) {
this.encodedQueryNamesAndValues = encodedQuery != null
? queryStringToNamesAndValues(
- canonicalize(encodedQuery, QUERY_ENCODE_SET, true, false, true, true))
+ canonicalize(encodedQuery, QUERY_ENCODE_SET, true, true, true))
: null;
return this;
}
@@ -805,9 +792,9 @@ public final class HttpUrl {
if (name == null) throw new IllegalArgumentException("name == null");
if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>();
encodedQueryNamesAndValues.add(
- canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, false, true, true));
+ canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true, true));
encodedQueryNamesAndValues.add(value != null
- ? canonicalize(value, QUERY_COMPONENT_ENCODE_SET, false, false, true, true)
+ ? canonicalize(value, QUERY_COMPONENT_ENCODE_SET, false, true, true)
: null);
return this;
}
@@ -817,9 +804,9 @@ public final class HttpUrl {
if (encodedName == null) throw new IllegalArgumentException("encodedName == null");
if (encodedQueryNamesAndValues == null) encodedQueryNamesAndValues = new ArrayList<>();
encodedQueryNamesAndValues.add(
- canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, false, true, true));
+ canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true, true));
encodedQueryNamesAndValues.add(encodedValue != null
- ? canonicalize(encodedValue, QUERY_COMPONENT_ENCODE_SET, true, false, true, true)
+ ? canonicalize(encodedValue, QUERY_COMPONENT_ENCODE_SET, true, true, true)
: null);
return this;
}
@@ -839,8 +826,7 @@ public final class HttpUrl {
public Builder removeAllQueryParameters(String name) {
if (name == null) throw new IllegalArgumentException("name == null");
if (encodedQueryNamesAndValues == null) return this;
- String nameToRemove = canonicalize(
- name, QUERY_COMPONENT_ENCODE_SET, false, false, true, true);
+ String nameToRemove = canonicalize(name, QUERY_COMPONENT_ENCODE_SET, false, true, true);
removeAllCanonicalQueryParameters(nameToRemove);
return this;
}
@@ -849,7 +835,7 @@ public final class HttpUrl {
if (encodedName == null) throw new IllegalArgumentException("encodedName == null");
if (encodedQueryNamesAndValues == null) return this;
removeAllCanonicalQueryParameters(
- canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, false, true, true));
+ canonicalize(encodedName, QUERY_COMPONENT_ENCODE_SET, true, true, true));
return this;
}
@@ -868,14 +854,14 @@ public final class HttpUrl {
public Builder fragment(String fragment) {
this.encodedFragment = fragment != null
- ? canonicalize(fragment, FRAGMENT_ENCODE_SET, false, false, false, false)
+ ? canonicalize(fragment, FRAGMENT_ENCODE_SET, false, false, false)
: null;
return this;
}
public Builder encodedFragment(String encodedFragment) {
this.encodedFragment = encodedFragment != null
- ? canonicalize(encodedFragment, FRAGMENT_ENCODE_SET, true, false, false, false)
+ ? canonicalize(encodedFragment, FRAGMENT_ENCODE_SET, true, false, false)
: null;
return this;
}
@@ -888,20 +874,20 @@ public final class HttpUrl {
for (int i = 0, size = encodedPathSegments.size(); i < size; i++) {
String pathSegment = encodedPathSegments.get(i);
encodedPathSegments.set(i,
- canonicalize(pathSegment, PATH_SEGMENT_ENCODE_SET_URI, true, true, false, true));
+ canonicalize(pathSegment, PATH_SEGMENT_ENCODE_SET_URI, true, false, true));
}
if (encodedQueryNamesAndValues != null) {
for (int i = 0, size = encodedQueryNamesAndValues.size(); i < size; i++) {
String component = encodedQueryNamesAndValues.get(i);
if (component != null) {
encodedQueryNamesAndValues.set(i,
- canonicalize(component, QUERY_COMPONENT_ENCODE_SET_URI, true, true, true, true));
+ canonicalize(component, QUERY_COMPONENT_ENCODE_SET_URI, true, true, true));
}
}
}
if (encodedFragment != null) {
encodedFragment = canonicalize(
- encodedFragment, FRAGMENT_ENCODE_SET_URI, true, true, false, false);
+ encodedFragment, FRAGMENT_ENCODE_SET_URI, true, false, false);
}
return this;
}
@@ -1014,19 +1000,19 @@ public final class HttpUrl {
int passwordColonOffset = delimiterOffset(
input, pos, componentDelimiterOffset, ":");
String canonicalUsername = canonicalize(
- input, pos, passwordColonOffset, USERNAME_ENCODE_SET, true, false, false, true);
+ input, pos, passwordColonOffset, USERNAME_ENCODE_SET, true, false, true);
this.encodedUsername = hasUsername
? this.encodedUsername + "%40" + canonicalUsername
: canonicalUsername;
if (passwordColonOffset != componentDelimiterOffset) {
hasPassword = true;
this.encodedPassword = canonicalize(input, passwordColonOffset + 1,
- componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, false, true);
+ componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, true);
}
hasUsername = true;
} else {
- this.encodedPassword = this.encodedPassword + "%40" + canonicalize(input, pos,
- componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, false, true);
+ this.encodedPassword = this.encodedPassword + "%40" + canonicalize(
+ input, pos, componentDelimiterOffset, PASSWORD_ENCODE_SET, true, false, true);
}
pos = componentDelimiterOffset + 1;
break;
@@ -1073,14 +1059,14 @@ public final class HttpUrl {
if (pos < limit && input.charAt(pos) == '?') {
int queryDelimiterOffset = delimiterOffset(input, pos, limit, "#");
this.encodedQueryNamesAndValues = queryStringToNamesAndValues(canonicalize(
- input, pos + 1, queryDelimiterOffset, QUERY_ENCODE_SET, true, false, true, true));
+ input, pos + 1, queryDelimiterOffset, QUERY_ENCODE_SET, true, true, true));
pos = queryDelimiterOffset;
}
// Fragment.
if (pos < limit && input.charAt(pos) == '#') {
this.encodedFragment = canonicalize(
- input, pos + 1, limit, FRAGMENT_ENCODE_SET, true, false, false, false);
+ input, pos + 1, limit, FRAGMENT_ENCODE_SET, true, false, false);
}
return ParseResult.SUCCESS;
@@ -1117,7 +1103,7 @@ public final class HttpUrl {
private void push(String input, int pos, int limit, boolean addTrailingSlash,
boolean alreadyEncoded) {
String segment = canonicalize(
- input, pos, limit, PATH_SEGMENT_ENCODE_SET, alreadyEncoded, false, false, true);
+ input, pos, limit, PATH_SEGMENT_ENCODE_SET, alreadyEncoded, false, true);
if (isDot(segment)) {
return; // Skip '.' path segments.
}
@@ -1468,7 +1454,7 @@ public final class HttpUrl {
private static int parsePort(String input, int pos, int limit) {
try {
// Canonicalize the port string to skip '\n' etc.
- String portString = canonicalize(input, pos, limit, "", false, false, false, true);
+ String portString = canonicalize(input, pos, limit, "", false, false, true);
int i = Integer.parseInt(portString);
if (i > 0 && i <= 65535) return i;
return -1;
@@ -1537,13 +1523,6 @@ public final class HttpUrl {
}
}
- static boolean percentEncoded(String encoded, int pos, int limit) {
- return pos + 2 < limit
- && encoded.charAt(pos) == '%'
- && decodeHexDigit(encoded.charAt(pos + 1)) != -1
- && decodeHexDigit(encoded.charAt(pos + 2)) != -1;
- }
-
static int decodeHexDigit(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
@@ -1563,26 +1542,24 @@ public final class HttpUrl {
* </ul>
*
* @param alreadyEncoded true to leave '%' as-is; false to convert it to '%25'.
- * @param strict true to encode '%' if it is not the prefix of a valid percent encoding.
- * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded
+ * @param plusIsSpace true to encode '+' as "%2B" if it is not already encoded.
* @param asciiOnly true to encode all non-ASCII codepoints.
*/
static String canonicalize(String input, int pos, int limit, String encodeSet,
- boolean alreadyEncoded, boolean strict, boolean plusIsSpace, boolean asciiOnly) {
+ boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) {
int codePoint;
for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
codePoint = input.codePointAt(i);
if (codePoint < 0x20
|| codePoint == 0x7f
- || codePoint >= 0x80 && asciiOnly
+ || (codePoint >= 0x80 && asciiOnly)
|| encodeSet.indexOf(codePoint) != -1
- || codePoint == '%' && (!alreadyEncoded || strict && !percentEncoded(input, i, limit))
- || codePoint == '+' && plusIsSpace) {
+ || (codePoint == '%' && !alreadyEncoded)
+ || (codePoint == '+' && plusIsSpace)) {
// Slow path: the character at i requires encoding!
Buffer out = new Buffer();
out.writeUtf8(input, pos, i);
- canonicalize(out, input, i, limit, encodeSet, alreadyEncoded, strict, plusIsSpace,
- asciiOnly);
+ canonicalize(out, input, i, limit, encodeSet, alreadyEncoded, plusIsSpace, asciiOnly);
return out.readUtf8();
}
}
@@ -1591,8 +1568,8 @@ public final class HttpUrl {
return input.substring(pos, limit);
}
- static void canonicalize(Buffer out, String input, int pos, int limit, String encodeSet,
- boolean alreadyEncoded, boolean strict, boolean plusIsSpace, boolean asciiOnly) {
+ static void canonicalize(Buffer out, String input, int pos, int limit,
+ String encodeSet, boolean alreadyEncoded, boolean plusIsSpace, boolean asciiOnly) {
Buffer utf8Buffer = null; // Lazily allocated.
int codePoint;
for (int i = pos; i < limit; i += Character.charCount(codePoint)) {
@@ -1605,9 +1582,9 @@ public final class HttpUrl {
out.writeUtf8(alreadyEncoded ? "+" : "%2B");
} else if (codePoint < 0x20
|| codePoint == 0x7f
- || codePoint >= 0x80 && asciiOnly
+ || (codePoint >= 0x80 && asciiOnly)
|| encodeSet.indexOf(codePoint) != -1
- || codePoint == '%' && (!alreadyEncoded || strict && !percentEncoded(input, i, limit))) {
+ || (codePoint == '%' && !alreadyEncoded)) {
// Percent encode this character.
if (utf8Buffer == null) {
utf8Buffer = new Buffer();
@@ -1626,9 +1603,9 @@ public final class HttpUrl {
}
}
- static String canonicalize(String input, String encodeSet, boolean alreadyEncoded, boolean strict,
+ static String canonicalize(String input, String encodeSet, boolean alreadyEncoded,
boolean plusIsSpace, boolean asciiOnly) {
return canonicalize(
- input, 0, input.length(), encodeSet, alreadyEncoded, strict, plusIsSpace, asciiOnly);
+ input, 0, input.length(), encodeSet, alreadyEncoded, plusIsSpace, asciiOnly);
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
index 4ed8000..aabc2d2 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/OkHttpClient.java
@@ -17,15 +17,12 @@ 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.AuthenticatorAdapter;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
-import java.io.IOException;
import java.net.CookieHandler;
import java.net.MalformedURLException;
import java.net.Proxy;
@@ -41,8 +38,6 @@ import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
-import okio.BufferedSink;
-import okio.BufferedSource;
/**
* Configures and creates HTTP connections. Most applications can use a single
@@ -64,35 +59,6 @@ public class OkHttpClient implements Cloneable {
static {
Internal.instance = new Internal() {
- @Override public Transport newTransport(
- Connection connection, HttpEngine httpEngine) throws IOException {
- return connection.newTransport(httpEngine);
- }
-
- @Override public boolean clearOwner(Connection connection) {
- return connection.clearOwner();
- }
-
- @Override public void closeIfOwnedBy(Connection connection, Object owner) throws IOException {
- connection.closeIfOwnedBy(owner);
- }
-
- @Override public int recycleCount(Connection connection) {
- return connection.recycleCount();
- }
-
- @Override public void setProtocol(Connection connection, Protocol protocol) {
- connection.setProtocol(protocol);
- }
-
- @Override public void setOwner(Connection connection, HttpEngine httpEngine) {
- connection.setOwner(httpEngine);
- }
-
- @Override public boolean isReadable(Connection pooled) {
- return pooled.isReadable();
- }
-
@Override public void addLenient(Headers.Builder builder, String line) {
builder.addLenient(line);
}
@@ -109,25 +75,22 @@ public class OkHttpClient implements Cloneable {
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 boolean connectionBecameIdle(
+ ConnectionPool pool, RealConnection connection) {
+ return pool.connectionBecameIdle(connection);
}
- @Override public Network network(OkHttpClient client) {
- return client.network;
+ @Override public RealConnection get(
+ ConnectionPool pool, Address address, StreamAllocation streamAllocation) {
+ return pool.get(address, streamAllocation);
}
- @Override public void setNetwork(OkHttpClient client, Network network) {
- client.network = network;
+ @Override public void put(ConnectionPool pool, RealConnection connection) {
+ pool.put(connection);
}
- @Override public void connectAndSetOwner(OkHttpClient client, Connection connection,
- HttpEngine owner, Request request) throws RouteException {
- connection.connectAndSetOwner(client, owner, request);
+ @Override public RouteDatabase routeDatabase(ConnectionPool connectionPool) {
+ return connectionPool.routeDatabase;
}
@Override
@@ -135,24 +98,8 @@ public class OkHttpClient implements Cloneable {
call.enqueue(responseCallback, forWebSocket);
}
- @Override public void callEngineReleaseConnection(Call call) throws IOException {
- call.engine.releaseConnection();
- }
-
- @Override public Connection callEngineGetConnection(Call call) {
- return call.engine.getConnection();
- }
-
- @Override public BufferedSource connectionRawSource(Connection connection) {
- return connection.rawSource();
- }
-
- @Override public BufferedSink connectionRawSink(Connection connection) {
- return connection.rawSink();
- }
-
- @Override public void connectionSetOwner(Connection connection, Object owner) {
- connection.setOwner(owner);
+ @Override public StreamAllocation callEngineGetStreamAllocation(Call call) {
+ return call.engine.streamAllocation;
}
@Override
@@ -190,7 +137,7 @@ public class OkHttpClient implements Cloneable {
private CertificatePinner certificatePinner;
private Authenticator authenticator;
private ConnectionPool connectionPool;
- private Network network;
+ private Dns dns;
private boolean followSslRedirects = true;
private boolean followRedirects = true;
private boolean retryOnConnectionFailure = true;
@@ -221,7 +168,7 @@ public class OkHttpClient implements Cloneable {
this.certificatePinner = okHttpClient.certificatePinner;
this.authenticator = okHttpClient.authenticator;
this.connectionPool = okHttpClient.connectionPool;
- this.network = okHttpClient.network;
+ this.dns = okHttpClient.dns;
this.followSslRedirects = okHttpClient.followSslRedirects;
this.followRedirects = okHttpClient.followRedirects;
this.retryOnConnectionFailure = okHttpClient.retryOnConnectionFailure;
@@ -358,6 +305,20 @@ public class OkHttpClient implements Cloneable {
}
/**
+ * Sets the DNS service used to lookup IP addresses for hostnames.
+ *
+ * <p>If unset, the {@link Dns#SYSTEM system-wide default} DNS will be used.
+ */
+ public OkHttpClient setDns(Dns dns) {
+ this.dns = dns;
+ return this;
+ }
+
+ public Dns getDns() {
+ return dns;
+ }
+
+ /**
* Sets the socket factory used to create connections. OkHttp only uses
* the parameterless {@link SocketFactory#createSocket() createSocket()}
* method to create unconnected sockets. Overriding this method,
@@ -647,8 +608,8 @@ public class OkHttpClient implements Cloneable {
if (result.connectionSpecs == null) {
result.connectionSpecs = DEFAULT_CONNECTION_SPECS;
}
- if (result.network == null) {
- result.network = Network.DEFAULT;
+ if (result.dns == null) {
+ result.dns = Dns.SYSTEM;
}
return result;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Request.java b/okhttp/src/main/java/com/squareup/okhttp/Request.java
index 2417c13..e099267 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Request.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Request.java
@@ -189,6 +189,9 @@ public final class Request {
/**
* Adds a header with {@code name} and {@code value}. Prefer this method for
* multiply-valued headers like "Cookie".
+ *
+ * <p>Note that for some headers including {@code Content-Length} and {@code Content-Encoding},
+ * OkHttp may replace {@code value} with a header derived from the request body.
*/
public Builder addHeader(String name, String value) {
headers.add(name, value);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
index bfa95c4..512aa0d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/TlsVersion.java
@@ -30,7 +30,7 @@ public enum TlsVersion {
final String javaName;
- private TlsVersion(String javaName) {
+ TlsVersion(String javaName) {
this.javaName = javaName;
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
index 21bcbf5..03bc1c5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Internal.java
@@ -15,26 +15,20 @@
*/
package com.squareup.okhttp.internal;
+import com.squareup.okhttp.Address;
import com.squareup.okhttp.Call;
import com.squareup.okhttp.Callback;
-import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.ConnectionSpec;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.HttpUrl;
import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Protocol;
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.internal.http.HttpEngine;
-import com.squareup.okhttp.internal.http.RouteException;
-import com.squareup.okhttp.internal.http.Transport;
-import java.io.IOException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.io.RealConnection;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.util.logging.Logger;
import javax.net.ssl.SSLSocket;
-import okio.BufferedSink;
-import okio.BufferedSource;
/**
* Escalate internal APIs in {@code com.squareup.okhttp} so they can be used
@@ -51,21 +45,6 @@ public abstract class Internal {
public static Internal instance;
- public abstract Transport newTransport(Connection connection, HttpEngine httpEngine)
- throws IOException;
-
- public abstract boolean clearOwner(Connection connection);
-
- public abstract void closeIfOwnedBy(Connection connection, Object owner) throws IOException;
-
- public abstract int recycleCount(Connection connection);
-
- public abstract void setProtocol(Connection connection, Protocol protocol);
-
- public abstract void setOwner(Connection connection, HttpEngine httpEngine);
-
- public abstract boolean isReadable(Connection pooled);
-
public abstract void addLenient(Headers.Builder builder, String line);
public abstract void addLenient(Headers.Builder builder, String name, String value);
@@ -74,16 +53,14 @@ public abstract class Internal {
public abstract InternalCache internalCache(OkHttpClient client);
- public abstract void recycle(ConnectionPool pool, Connection connection);
-
- public abstract RouteDatabase routeDatabase(OkHttpClient client);
+ public abstract RealConnection get(
+ ConnectionPool pool, Address address, StreamAllocation streamAllocation);
- public abstract Network network(OkHttpClient client);
+ public abstract void put(ConnectionPool pool, RealConnection connection);
- public abstract void setNetwork(OkHttpClient client, Network network);
+ public abstract boolean connectionBecameIdle(ConnectionPool pool, RealConnection connection);
- public abstract void connectAndSetOwner(OkHttpClient client, Connection connection,
- HttpEngine owner, Request request) throws RouteException;
+ public abstract RouteDatabase routeDatabase(ConnectionPool connectionPool);
public abstract void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket,
boolean isFallback);
@@ -93,9 +70,5 @@ public abstract class Internal {
// TODO delete the following when web sockets move into the main package.
public abstract void callEnqueue(Call call, Callback responseCallback, boolean forWebSocket);
- public abstract void callEngineReleaseConnection(Call call) throws IOException;
- public abstract Connection callEngineGetConnection(Call call);
- public abstract BufferedSource connectionRawSource(Connection connection);
- public abstract BufferedSink connectionRawSink(Connection connection);
- public abstract void connectionSetOwner(Connection connection, Object owner);
+ public abstract StreamAllocation callEngineGetStreamAllocation(Call call);
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
deleted file mode 100644
index a007065..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Network.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (C) 2012 Square, Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.squareup.okhttp.internal;
-
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-
-/**
- * Services specific to the host device's network interface. Prefer this over {@link
- * InetAddress#getAllByName} to make code more testable.
- */
-public interface Network {
- Network DEFAULT = new Network() {
- @Override public InetAddress[] resolveInetAddresses(String host) throws UnknownHostException {
- if (host == null) throw new UnknownHostException("host == null");
- return InetAddress.getAllByName(host);
- }
- };
-
- InetAddress[] resolveInetAddresses(String host) throws UnknownHostException;
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
index b906495..a2df181 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -16,8 +16,13 @@
*/
package com.squareup.okhttp.internal;
+import android.util.Log;
import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.internal.tls.AndroidTrustRootIndex;
+import com.squareup.okhttp.internal.tls.RealTrustRootIndex;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
import java.io.IOException;
+import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -29,6 +34,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
import okio.Buffer;
import static com.squareup.okhttp.internal.Internal.logger;
@@ -50,6 +57,11 @@ import static com.squareup.okhttp.internal.Internal.logger;
* unstable.
*
* Supported on OpenJDK 7 and 8 (via the JettyALPN-boot library).
+ *
+ * <h3>Trust Manager Extraction</h3>
+ *
+ * <p>Supported on Android 2.3+ and OpenJDK 7+. There are no public APIs to recover the trust
+ * manager that was used to create an {@link SSLSocketFactory}.
*/
public class Platform {
private static final Platform PLATFORM = findPlatform();
@@ -73,6 +85,14 @@ public class Platform {
public void untagSocket(Socket socket) throws SocketException {
}
+ public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ return null;
+ }
+
+ public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+ return new RealTrustRootIndex(trustManager.getAcceptedIssuers());
+ }
+
/**
* Configure TLS extensions on {@code sslSocket} for {@code route}.
*
@@ -100,15 +120,21 @@ public class Platform {
socket.connect(address, connectTimeout);
}
+ public void log(String message) {
+ System.out.println(message);
+ }
+
/** Attempt to match the host runtime to a capable Platform implementation. */
private static Platform findPlatform() {
// Attempt to find Android 2.3+ APIs.
try {
+ Class<?> sslParametersClass;
try {
- Class.forName("com.android.org.conscrypt.OpenSSLSocketImpl");
+ sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
} catch (ClassNotFoundException e) {
// Older platform before being unbundled.
- Class.forName("org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
+ sslParametersClass = Class.forName(
+ "org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
}
OptionalMethod<Socket> setUseSessionTickets
@@ -136,25 +162,34 @@ public class Platform {
} catch (ClassNotFoundException | NoSuchMethodException ignored) {
}
- return new Android(setUseSessionTickets, setHostname, trafficStatsTagSocket,
- trafficStatsUntagSocket, getAlpnSelectedProtocol, setAlpnProtocols);
+ return new Android(sslParametersClass, setUseSessionTickets, setHostname,
+ trafficStatsTagSocket, trafficStatsUntagSocket, getAlpnSelectedProtocol,
+ setAlpnProtocols);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
}
- // Find Jetty's ALPN extension for OpenJDK.
+ // Find an Oracle JDK.
try {
- String negoClassName = "org.eclipse.jetty.alpn.ALPN";
- Class<?> negoClass = Class.forName(negoClassName);
- Class<?> providerClass = Class.forName(negoClassName + "$Provider");
- Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
- Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
- Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
- Method getMethod = negoClass.getMethod("get", SSLSocket.class);
- Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
- return new JdkWithJettyBootPlatform(
- putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
- } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+ Class<?> sslContextClass = Class.forName("sun.security.ssl.SSLContextImpl");
+
+ // Find Jetty's ALPN extension for OpenJDK.
+ try {
+ String negoClassName = "org.eclipse.jetty.alpn.ALPN";
+ Class<?> negoClass = Class.forName(negoClassName);
+ Class<?> providerClass = Class.forName(negoClassName + "$Provider");
+ Class<?> clientProviderClass = Class.forName(negoClassName + "$ClientProvider");
+ Class<?> serverProviderClass = Class.forName(negoClassName + "$ServerProvider");
+ Method putMethod = negoClass.getMethod("put", SSLSocket.class, providerClass);
+ Method getMethod = negoClass.getMethod("get", SSLSocket.class);
+ Method removeMethod = negoClass.getMethod("remove", SSLSocket.class);
+ return new JdkWithJettyBootPlatform(sslContextClass,
+ putMethod, getMethod, removeMethod, clientProviderClass, serverProviderClass);
+ } catch (ClassNotFoundException | NoSuchMethodException ignored) {
+ }
+
+ return new JdkPlatform(sslContextClass);
+ } catch (ClassNotFoundException ignored) {
}
return new Platform();
@@ -162,6 +197,9 @@ public class Platform {
/** Android 2.3 or better. */
private static class Android extends Platform {
+ private static final int MAX_LOG_LENGTH = 4000;
+
+ private final Class<?> sslParametersClass;
private final OptionalMethod<Socket> setUseSessionTickets;
private final OptionalMethod<Socket> setHostname;
@@ -173,9 +211,11 @@ public class Platform {
private final OptionalMethod<Socket> getAlpnSelectedProtocol;
private final OptionalMethod<Socket> setAlpnProtocols;
- public Android(OptionalMethod<Socket> setUseSessionTickets, OptionalMethod<Socket> setHostname,
- Method trafficStatsTagSocket, Method trafficStatsUntagSocket,
- OptionalMethod<Socket> getAlpnSelectedProtocol, OptionalMethod<Socket> setAlpnProtocols) {
+ public Android(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
+ OptionalMethod<Socket> setHostname, Method trafficStatsTagSocket,
+ Method trafficStatsUntagSocket, OptionalMethod<Socket> getAlpnSelectedProtocol,
+ OptionalMethod<Socket> setAlpnProtocols) {
+ this.sslParametersClass = sslParametersClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
this.trafficStatsTagSocket = trafficStatsTagSocket;
@@ -188,15 +228,46 @@ public class Platform {
int connectTimeout) throws IOException {
try {
socket.connect(address, connectTimeout);
- } catch (SecurityException se) {
+ } catch (AssertionError e) {
+ if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+ throw e;
+ } catch (SecurityException e) {
// Before android 4.3, socket.connect could throw a SecurityException
// if opening a socket resulted in an EACCES error.
IOException ioException = new IOException("Exception in connect");
- ioException.initCause(se);
+ ioException.initCause(e);
throw ioException;
}
}
+ @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ Object context = readFieldOrNull(sslSocketFactory, sslParametersClass, "sslParameters");
+ if (context == null) {
+ // If that didn't work, try the Google Play Services SSL provider before giving up. This
+ // must be loaded by the SSLSocketFactory's class loader.
+ try {
+ Class<?> gmsSslParametersClass = Class.forName(
+ "com.google.android.gms.org.conscrypt.SSLParametersImpl", false,
+ sslSocketFactory.getClass().getClassLoader());
+ context = readFieldOrNull(sslSocketFactory, gmsSslParametersClass, "sslParameters");
+ } catch (ClassNotFoundException e) {
+ return null;
+ }
+ }
+
+ X509TrustManager x509TrustManager = readFieldOrNull(
+ context, X509TrustManager.class, "x509TrustManager");
+ if (x509TrustManager != null) return x509TrustManager;
+
+ return readFieldOrNull(context, X509TrustManager.class, "trustManager");
+ }
+
+ @Override public TrustRootIndex trustRootIndex(X509TrustManager trustManager) {
+ TrustRootIndex result = AndroidTrustRootIndex.get(trustManager);
+ if (result != null) return result;
+ return super.trustRootIndex(trustManager);
+ }
+
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
// Enable SNI and session tickets.
@@ -243,20 +314,49 @@ public class Platform {
throw new RuntimeException(e.getCause());
}
}
+
+ @Override public void log(String message) {
+ // Split by line, then ensure each line can fit into Log's maximum length.
+ for (int i = 0, length = message.length(); i < length; i++) {
+ int newline = message.indexOf('\n', i);
+ newline = newline != -1 ? newline : length;
+ do {
+ int end = Math.min(newline, i + MAX_LOG_LENGTH);
+ Log.d("OkHttp", message.substring(i, end));
+ i = end;
+ } while (i < newline);
+ }
+ }
+ }
+
+ /** JDK 1.7 or better. */
+ private static class JdkPlatform extends Platform {
+ private final Class<?> sslContextClass;
+
+ public JdkPlatform(Class<?> sslContextClass) {
+ this.sslContextClass = sslContextClass;
+ }
+
+ @Override public X509TrustManager trustManager(SSLSocketFactory sslSocketFactory) {
+ Object context = readFieldOrNull(sslSocketFactory, sslContextClass, "context");
+ if (context == null) return null;
+ return readFieldOrNull(context, X509TrustManager.class, "trustManager");
+ }
}
/**
* OpenJDK 7+ with {@code org.mortbay.jetty.alpn/alpn-boot} in the boot class path.
*/
- private static class JdkWithJettyBootPlatform extends Platform {
+ private static class JdkWithJettyBootPlatform extends JdkPlatform {
private final Method putMethod;
private final Method getMethod;
private final Method removeMethod;
private final Class<?> clientProviderClass;
private final Class<?> serverProviderClass;
- public JdkWithJettyBootPlatform(Method putMethod, Method getMethod, Method removeMethod,
- Class<?> clientProviderClass, Class<?> serverProviderClass) {
+ public JdkWithJettyBootPlatform(Class<?> sslContextClass, Method putMethod, Method getMethod,
+ Method removeMethod, Class<?> clientProviderClass, Class<?> serverProviderClass) {
+ super(sslContextClass);
this.putMethod = putMethod;
this.getMethod = getMethod;
this.removeMethod = removeMethod;
@@ -368,4 +468,27 @@ public class Platform {
}
return result.readByteArray();
}
+
+ static <T> T readFieldOrNull(Object instance, Class<T> fieldType, String fieldName) {
+ for (Class<?> c = instance.getClass(); c != Object.class; c = c.getSuperclass()) {
+ try {
+ Field field = c.getDeclaredField(fieldName);
+ field.setAccessible(true);
+ Object value = field.get(instance);
+ if (value == null || !fieldType.isInstance(value)) return null;
+ return fieldType.cast(value);
+ } catch (NoSuchFieldException ignored) {
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ }
+ }
+
+ // Didn't find the field we wanted. As a last gasp attempt, try to find the value on a delegate.
+ if (!fieldName.equals("delegate")) {
+ Object delegate = readFieldOrNull(instance, Object.class, "delegate");
+ if (delegate != null) return readFieldOrNull(delegate, fieldType, fieldName);
+ }
+
+ return null;
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
index efc26ec..b05dc6d 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Util.java
@@ -288,4 +288,15 @@ public final class Util {
return e.getCause() != null && e.getMessage() != null
&& e.getMessage().contains("getsockname failed");
}
+
+ public static boolean contains(String[] array, String value) {
+ return Arrays.asList(array).contains(value);
+ }
+
+ public static String[] concat(String[] array, String value) {
+ String[] result = new String[array.length + 1];
+ System.arraycopy(array, 0, result, 0, array.length);
+ result[result.length - 1] = value;
+ return result;
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
index a86924b..6feb16c 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/FramedConnection.java
@@ -35,6 +35,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import okio.Buffer;
+import okio.BufferedSink;
import okio.BufferedSource;
import okio.ByteString;
import okio.Okio;
@@ -76,10 +77,10 @@ public final class FramedConnection implements Closeable {
final boolean client;
/**
- * User code to run in response to an incoming stream. Callbacks must not be
- * run on the callback executor.
+ * User code to run in response to incoming streams or settings. Calls to this are always invoked
+ * on {@link #executor}.
*/
- private final IncomingStreamHandler handler;
+ private final Listener listener;
private final Map<Integer, FramedStream> streams = new HashMap<>();
private final String hostName;
private int lastGoodStreamId;
@@ -111,9 +112,8 @@ public final class FramedConnection implements Closeable {
long bytesLeftInWriteWindow;
/** Settings we communicate to the peer. */
- // TODO: Do we want to dynamically adjust settings, or KISS and only set once?
- final Settings okHttpSettings = new Settings();
- // okHttpSettings.set(Settings.MAX_CONCURRENT_STREAMS, 0, max);
+ Settings okHttpSettings = new Settings();
+
private static final int OKHTTP_CLIENT_WINDOW_SIZE = 16 * 1024 * 1024;
/** Settings we receive from the peer. */
@@ -132,7 +132,7 @@ public final class FramedConnection implements Closeable {
protocol = builder.protocol;
pushObserver = builder.pushObserver;
client = builder.client;
- handler = builder.handler;
+ listener = builder.listener;
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-5.1.1
nextStreamId = builder.client ? 1 : 2;
if (builder.client && protocol == Protocol.HTTP_2) {
@@ -168,9 +168,9 @@ public final class FramedConnection implements Closeable {
}
bytesLeftInWriteWindow = peerSettings.getInitialWindowSize(DEFAULT_INITIAL_WINDOW_SIZE);
socket = builder.socket;
- frameWriter = variant.newWriter(Okio.buffer(Okio.sink(builder.socket)), client);
+ frameWriter = variant.newWriter(builder.sink, client);
- readerRunnable = new Reader();
+ readerRunnable = new Reader(variant.newReader(builder.source, client));
new Thread(readerRunnable).start(); // Not a daemon thread.
}
@@ -209,6 +209,10 @@ public final class FramedConnection implements Closeable {
return idleStartTimeNs != Long.MAX_VALUE;
}
+ public synchronized int maxConcurrentStreams() {
+ return peerSettings.getMaxConcurrentStreams(Integer.MAX_VALUE);
+ }
+
/**
* Returns the time in ns when this connection became idle or Long.MAX_VALUE
* if connection is not idle.
@@ -515,30 +519,53 @@ public final class FramedConnection implements Closeable {
}
}
+ /** Merges {@code settings} into this peer's settings and sends them to the remote peer. */
+ public void setSettings(Settings settings) throws IOException {
+ synchronized (frameWriter) {
+ synchronized (this) {
+ if (shutdown) {
+ throw new IOException("shutdown");
+ }
+ okHttpSettings.merge(settings);
+ frameWriter.settings(settings);
+ }
+ }
+ }
+
public static class Builder {
- private String hostName;
private Socket socket;
- private IncomingStreamHandler handler = IncomingStreamHandler.REFUSE_INCOMING_STREAMS;
+ private String hostName;
+ private BufferedSource source;
+ private BufferedSink sink;
+ private Listener listener = Listener.REFUSE_INCOMING_STREAMS;
private Protocol protocol = Protocol.SPDY_3;
private PushObserver pushObserver = PushObserver.CANCEL;
private boolean client;
- public Builder(boolean client, Socket socket) throws IOException {
- this(((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(), client, socket);
- }
-
/**
* @param client true if this peer initiated the connection; false if this
* peer accepted the connection.
*/
- public Builder(String hostName, boolean client, Socket socket) throws IOException {
- this.hostName = hostName;
+ public Builder(boolean client) throws IOException {
this.client = client;
+ }
+
+ public Builder socket(Socket socket) throws IOException {
+ return socket(socket, ((InetSocketAddress) socket.getRemoteSocketAddress()).getHostName(),
+ Okio.buffer(Okio.source(socket)), Okio.buffer(Okio.sink(socket)));
+ }
+
+ public Builder socket(
+ Socket socket, String hostName, BufferedSource source, BufferedSink sink) {
this.socket = socket;
+ this.hostName = hostName;
+ this.source = source;
+ this.sink = sink;
+ return this;
}
- public Builder handler(IncomingStreamHandler handler) {
- this.handler = handler;
+ public Builder listener(Listener listener) {
+ this.listener = listener;
return this;
}
@@ -562,17 +589,17 @@ public final class FramedConnection implements Closeable {
* write a frame, create an async task to do so.
*/
class Reader extends NamedRunnable implements FrameReader.Handler {
- FrameReader frameReader;
+ final FrameReader frameReader;
- private Reader() {
+ private Reader(FrameReader frameReader) {
super("OkHttp %s", hostName);
+ this.frameReader = frameReader;
}
@Override protected void execute() {
ErrorCode connectionErrorCode = ErrorCode.INTERNAL_ERROR;
ErrorCode streamErrorCode = ErrorCode.INTERNAL_ERROR;
try {
- frameReader = variant.newReader(Okio.buffer(Okio.source(socket)), client);
if (!client) {
frameReader.readConnectionPreface();
}
@@ -645,9 +672,9 @@ public final class FramedConnection implements Closeable {
executor.execute(new NamedRunnable("OkHttp %s stream %d", hostName, streamId) {
@Override public void execute() {
try {
- handler.receive(newStream);
+ listener.onStream(newStream);
} catch (IOException e) {
- logger.log(Level.INFO, "StreamHandler failure for " + hostName, e);
+ logger.log(Level.INFO, "FramedConnection.Listener failure for " + hostName, e);
try {
newStream.close(ErrorCode.PROTOCOL_ERROR);
} catch (IOException ignored) {
@@ -703,6 +730,11 @@ public final class FramedConnection implements Closeable {
streamsToNotify = streams.values().toArray(new FramedStream[streams.size()]);
}
}
+ executor.execute(new NamedRunnable("OkHttp %s settings", hostName) {
+ @Override public void execute() {
+ listener.onSettings(FramedConnection.this);
+ }
+ });
}
if (streamsToNotify != null && delta != 0) {
for (FramedStream stream : streamsToNotify) {
@@ -878,4 +910,34 @@ public final class FramedConnection implements Closeable {
}
});
}
+
+ /** Listener of streams and settings initiated by the peer. */
+ public abstract static class Listener {
+ public static final Listener REFUSE_INCOMING_STREAMS = new Listener() {
+ @Override public void onStream(FramedStream stream) throws IOException {
+ stream.close(ErrorCode.REFUSED_STREAM);
+ }
+ };
+
+ /**
+ * Handle a new stream from this connection's peer. Implementations should
+ * respond by either {@linkplain FramedStream#reply replying to the stream}
+ * or {@linkplain FramedStream#close closing it}. This response does not
+ * need to be synchronous.
+ */
+ public abstract void onStream(FramedStream stream) throws IOException;
+
+ /**
+ * Notification that the connection's peer's settings may have changed.
+ * Implementations should take appropriate action to handle the updated
+ * settings.
+ *
+ * <p>It is the implementation's responsibility to handle concurrent calls
+ * to this method. A remote peer that sends multiple settings frames will
+ * trigger multiple calls to this method, and those calls are not
+ * necessarily serialized.
+ */
+ public void onSettings(FramedConnection connection) {
+ }
+ }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java b/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
deleted file mode 100644
index 57863df..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/framed/IncomingStreamHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp.internal.framed;
-
-import java.io.IOException;
-
-/** Listener to be notified when a connected peer creates a new stream. */
-public interface IncomingStreamHandler {
- IncomingStreamHandler REFUSE_INCOMING_STREAMS = new IncomingStreamHandler() {
- @Override public void receive(FramedStream stream) throws IOException {
- stream.close(ErrorCode.REFUSED_STREAM);
- }
- };
-
- /**
- * Handle a new stream from this connection's peer. Implementations should
- * respond by either {@link FramedStream#reply replying to the stream} or
- * {@link FramedStream#close closing it}. This response does not need to be
- * synchronous.
- */
- void receive(FramedStream stream) throws IOException;
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
index 1bbde80..b43f0d3 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpConnection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http1xStream.java
@@ -16,17 +16,16 @@
package com.squareup.okhttp.internal.http;
-import com.squareup.okhttp.Connection;
-import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Headers;
+import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.internal.Internal;
import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.RealConnection;
import java.io.EOFException;
import java.io.IOException;
import java.net.ProtocolException;
-import java.net.Socket;
-import java.net.SocketTimeoutException;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
@@ -38,7 +37,6 @@ import 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;
/**
@@ -60,7 +58,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
* #newFixedLengthSource(long) newFixedLengthSource(0)} and may skip reading and
* closing that source.
*/
-public final class HttpConnection {
+public final class Http1xStream implements HttpStream {
private static final int STATE_IDLE = 0; // Idle connections are ready to write request headers.
private static final int STATE_OPEN_REQUEST_BODY = 1;
private static final int STATE_WRITING_REQUEST_BODY = 2;
@@ -69,63 +67,89 @@ public final class HttpConnection {
private static final int STATE_READING_RESPONSE_BODY = 5;
private static final int STATE_CLOSED = 6;
- private static final int ON_IDLE_HOLD = 0;
- private static final int ON_IDLE_POOL = 1;
- private static final int ON_IDLE_CLOSE = 2;
-
- private final ConnectionPool pool;
- private final Connection connection;
- private final Socket socket;
+ /** The stream allocation that owns this stream. May be null for HTTPS proxy tunnels. */
+ private final StreamAllocation streamAllocation;
private final BufferedSource source;
private final BufferedSink sink;
-
+ private HttpEngine httpEngine;
private int state = STATE_IDLE;
- private int onIdle = ON_IDLE_HOLD;
-
- public HttpConnection(ConnectionPool pool, Connection connection, Socket socket)
- throws IOException {
- this.pool = pool;
- this.connection = connection;
- this.socket = socket;
- this.source = Okio.buffer(Okio.source(socket));
- this.sink = Okio.buffer(Okio.sink(socket));
+
+ public Http1xStream(StreamAllocation streamAllocation, BufferedSource source, BufferedSink sink) {
+ this.streamAllocation = streamAllocation;
+ this.source = source;
+ this.sink = sink;
}
- public void setTimeouts(int readTimeoutMillis, int writeTimeoutMillis) {
- if (readTimeoutMillis != 0) {
- source.timeout().timeout(readTimeoutMillis, MILLISECONDS);
+ @Override public void setHttpEngine(HttpEngine httpEngine) {
+ this.httpEngine = httpEngine;
+ }
+
+ @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
+ if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
+ // Stream a request body of unknown length.
+ return newChunkedSink();
}
- if (writeTimeoutMillis != 0) {
- sink.timeout().timeout(writeTimeoutMillis, MILLISECONDS);
+
+ if (contentLength != -1) {
+ // Stream a request body of a known length.
+ return newFixedLengthSink(contentLength);
}
+
+ throw new IllegalStateException(
+ "Cannot stream a request body without chunked encoding or a known content length!");
+ }
+
+ @Override public void cancel() {
+ RealConnection connection = streamAllocation.connection();
+ if (connection != null) connection.cancel();
}
/**
- * Configure this connection to put itself back into the connection pool when
- * the HTTP response body is exhausted.
+ * Prepares the HTTP headers and sends them to the server.
+ *
+ * <p>For streaming requests with a body, headers must be prepared
+ * <strong>before</strong> the output stream has been written to. Otherwise
+ * the body would need to be buffered!
+ *
+ * <p>For non-streaming requests with a body, headers must be prepared
+ * <strong>after</strong> the output stream has been written to and closed.
+ * This ensures that the {@code Content-Length} header field receives the
+ * proper value.
*/
- public void poolOnIdle() {
- onIdle = ON_IDLE_POOL;
+ @Override public void writeRequestHeaders(Request request) throws IOException {
+ httpEngine.writingRequestHeaders();
+ String requestLine = RequestLine.get(
+ request, httpEngine.getConnection().getRoute().getProxy().type());
+ writeRequest(request.headers(), requestLine);
+ }
- // If we're already idle, go to the pool immediately.
- if (state == STATE_IDLE) {
- onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
- Internal.instance.recycle(pool, connection);
- }
+ @Override public Response.Builder readResponseHeaders() throws IOException {
+ return readResponse();
}
- /**
- * Configure this connection to close itself when the HTTP response body is
- * exhausted.
- */
- public void closeOnIdle() throws IOException {
- onIdle = ON_IDLE_CLOSE;
+ @Override public ResponseBody openResponseBody(Response response) throws IOException {
+ Source source = getTransferStream(response);
+ return new RealResponseBody(response.headers(), Okio.buffer(source));
+ }
- // If we're already idle, close immediately.
- if (state == STATE_IDLE) {
- state = STATE_CLOSED;
- connection.getSocket().close();
+ private Source getTransferStream(Response response) throws IOException {
+ if (!HttpEngine.hasBody(response)) {
+ return newFixedLengthSource(0);
+ }
+
+ if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
+ return newChunkedSource(httpEngine);
+ }
+
+ long contentLength = OkHeaders.contentLength(response);
+ if (contentLength != -1) {
+ return newFixedLengthSource(contentLength);
}
+
+ // Wrap the input stream from the connection (rather than just returning
+ // "socketIn" directly here), so that we can control its use after the
+ // reference escapes.
+ return newUnknownLengthSource();
}
/** Returns true if this connection is closed. */
@@ -133,39 +157,10 @@ public final class HttpConnection {
return state == STATE_CLOSED;
}
- public void closeIfOwnedBy(Object owner) throws IOException {
- Internal.instance.closeIfOwnedBy(connection, owner);
- }
-
- public void flush() throws IOException {
+ @Override public void finishRequest() throws IOException {
sink.flush();
}
- /** Returns the number of buffered bytes immediately readable. */
- public long bufferSize() {
- return source.buffer().size();
- }
-
- /** Test for a stale socket. */
- public boolean isReadable() {
- try {
- int readTimeout = socket.getSoTimeout();
- try {
- socket.setSoTimeout(1);
- if (source.exhausted()) {
- return false; // Stream is exhausted; socket is closed.
- }
- return true;
- } finally {
- socket.setSoTimeout(readTimeout);
- }
- } catch (SocketTimeoutException ignored) {
- return true; // Read timed out; socket is good.
- } catch (IOException e) {
- return false; // Couldn't read; socket is closed.
- }
- }
-
/** Returns bytes of a request header for sending on an HTTP transport. */
public void writeRequest(Headers headers, String requestLine) throws IOException {
if (state != STATE_IDLE) throw new IllegalStateException("state: " + state);
@@ -193,12 +188,8 @@ public final class HttpConnection {
Response.Builder responseBuilder = new Response.Builder()
.protocol(statusLine.protocol)
.code(statusLine.code)
- .message(statusLine.message);
-
- Headers.Builder headersBuilder = new Headers.Builder();
- readHeaders(headersBuilder);
- headersBuilder.add(OkHeaders.SELECTED_PROTOCOL, statusLine.protocol.toString());
- responseBuilder.headers(headersBuilder.build());
+ .message(statusLine.message)
+ .headers(readHeaders());
if (statusLine.code != HTTP_CONTINUE) {
state = STATE_OPEN_RESPONSE_BODY;
@@ -207,19 +198,20 @@ public final class HttpConnection {
}
} catch (EOFException e) {
// Provide more context if the server ends the stream before sending a response.
- IOException exception = new IOException("unexpected end of stream on " + connection
- + " (recycle count=" + Internal.instance.recycleCount(connection) + ")");
+ IOException exception = new IOException("unexpected end of stream on " + streamAllocation);
exception.initCause(e);
throw exception;
}
}
- /** Reads headers or trailers into {@code builder}. */
- public void readHeaders(Headers.Builder builder) throws IOException {
+ /** Reads headers or trailers. */
+ public Headers readHeaders() throws IOException {
+ Headers.Builder headers = new Headers.Builder();
// parse the result headers until the first blank line
for (String line; (line = source.readUtf8LineStrict()).length() != 0; ) {
- Internal.instance.addLenient(builder, line);
+ Internal.instance.addLenient(headers, line);
}
+ return headers.build();
}
public Sink newChunkedSink() {
@@ -234,7 +226,7 @@ public final class HttpConnection {
return new FixedLengthSink(contentLength);
}
- public void writeRequestBody(RetryableSink requestBody) throws IOException {
+ @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
if (state != STATE_OPEN_REQUEST_BODY) throw new IllegalStateException("state: " + state);
state = STATE_READ_RESPONSE_HEADERS;
requestBody.writeToSocket(sink);
@@ -254,18 +246,12 @@ public final class HttpConnection {
public Source newUnknownLengthSource() throws IOException {
if (state != STATE_OPEN_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
+ if (streamAllocation == null) throw new IllegalStateException("streamAllocation == null");
state = STATE_READING_RESPONSE_BODY;
+ streamAllocation.noNewStreams();
return new UnknownLengthSource();
}
- public BufferedSink rawSink() {
- return sink;
- }
-
- public BufferedSource rawSource() {
- return source;
- }
-
/**
* Sets the delegate of {@code timeout} to {@link Timeout#NONE} and resets its underlying timeout
* to the default configuration. Use this to avoid unexpected sharing of timeouts between pooled
@@ -366,36 +352,25 @@ public final class HttpConnection {
* Closes the cache entry and makes the socket available for reuse. This
* should be invoked when the end of the body has been reached.
*/
- protected final void endOfInput(boolean recyclable) throws IOException {
+ protected final void endOfInput() throws IOException {
if (state != STATE_READING_RESPONSE_BODY) throw new IllegalStateException("state: " + state);
detachTimeout(timeout);
- state = STATE_IDLE;
- if (recyclable && onIdle == ON_IDLE_POOL) {
- onIdle = ON_IDLE_HOLD; // Set the on idle policy back to the default.
- Internal.instance.recycle(pool, connection);
- } else if (onIdle == ON_IDLE_CLOSE) {
- state = STATE_CLOSED;
- connection.getSocket().close();
+ state = STATE_CLOSED;
+ if (streamAllocation != null) {
+ streamAllocation.streamFinished(Http1xStream.this);
}
}
- /**
- * Calls abort on the cache entry and disconnects the socket. This
- * should be invoked when the connection is closed unexpectedly to
- * invalidate the cache entry and to prevent the HTTP connection from
- * being reused. HTTP messages are sent in serial so whenever a message
- * cannot be read to completion, subsequent messages cannot be read
- * either and the connection must be discarded.
- *
- * <p>An earlier implementation skipped the remaining bytes, but this
- * requires that the entire transfer be completed. If the intention was
- * to cancel the transfer, closing the connection is the only solution.
- */
protected final void unexpectedEndOfInput() {
- Util.closeQuietly(connection.getSocket());
+ if (state == STATE_CLOSED) return;
+
state = STATE_CLOSED;
+ if (streamAllocation != null) {
+ streamAllocation.noNewStreams();
+ streamAllocation.streamFinished(Http1xStream.this);
+ }
}
}
@@ -406,7 +381,7 @@ public final class HttpConnection {
public FixedLengthSource(long length) throws IOException {
bytesRemaining = length;
if (bytesRemaining == 0) {
- endOfInput(true);
+ endOfInput();
}
}
@@ -423,7 +398,7 @@ public final class HttpConnection {
bytesRemaining -= read;
if (bytesRemaining == 0) {
- endOfInput(true);
+ endOfInput();
}
return read;
}
@@ -487,10 +462,8 @@ public final class HttpConnection {
}
if (bytesRemainingInChunk == 0L) {
hasMoreChunks = false;
- Headers.Builder trailersBuilder = new Headers.Builder();
- readHeaders(trailersBuilder);
- httpEngine.receiveHeaders(trailersBuilder.build());
- endOfInput(true);
+ httpEngine.receiveHeaders(readHeaders());
+ endOfInput();
}
}
@@ -516,7 +489,7 @@ public final class HttpConnection {
long read = source.read(sink, byteCount);
if (read == -1) {
inputExhausted = true;
- endOfInput(false);
+ endOfInput();
return -1;
}
return read;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java
index abeaf86..6b8b68f 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/FramedTransport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/Http2xStream.java
@@ -35,8 +35,10 @@ import java.util.Locale;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import okio.ByteString;
+import okio.ForwardingSource;
import okio.Okio;
import okio.Sink;
+import okio.Source;
import static com.squareup.okhttp.internal.framed.Header.RESPONSE_STATUS;
import static com.squareup.okhttp.internal.framed.Header.TARGET_AUTHORITY;
@@ -46,35 +48,77 @@ import static com.squareup.okhttp.internal.framed.Header.TARGET_PATH;
import static com.squareup.okhttp.internal.framed.Header.TARGET_SCHEME;
import static com.squareup.okhttp.internal.framed.Header.VERSION;
-public final class FramedTransport implements Transport {
+/** An HTTP stream for HTTP/2 and SPDY. */
+public final class Http2xStream implements HttpStream {
+ private static final ByteString CONNECTION = ByteString.encodeUtf8("connection");
+ private static final ByteString HOST = ByteString.encodeUtf8("host");
+ private static final ByteString KEEP_ALIVE = ByteString.encodeUtf8("keep-alive");
+ private static final ByteString PROXY_CONNECTION = ByteString.encodeUtf8("proxy-connection");
+ private static final ByteString TRANSFER_ENCODING = ByteString.encodeUtf8("transfer-encoding");
+ private static final ByteString TE = ByteString.encodeUtf8("te");
+ private static final ByteString ENCODING = ByteString.encodeUtf8("encoding");
+ private static final ByteString UPGRADE = ByteString.encodeUtf8("upgrade");
+
/** See http://www.chromium.org/spdy/spdy-protocol/spdy-protocol-draft3-1#TOC-3.2.1-Request. */
- private static final List<ByteString> SPDY_3_PROHIBITED_HEADERS = Util.immutableList(
- ByteString.encodeUtf8("connection"),
- ByteString.encodeUtf8("host"),
- ByteString.encodeUtf8("keep-alive"),
- ByteString.encodeUtf8("proxy-connection"),
- ByteString.encodeUtf8("transfer-encoding"));
+ private static final List<ByteString> SPDY_3_SKIPPED_REQUEST_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TRANSFER_ENCODING,
+ TARGET_METHOD,
+ TARGET_PATH,
+ TARGET_SCHEME,
+ TARGET_AUTHORITY,
+ TARGET_HOST,
+ VERSION);
+ private static final List<ByteString> SPDY_3_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TRANSFER_ENCODING);
/** See http://tools.ietf.org/html/draft-ietf-httpbis-http2-09#section-8.1.3. */
- private static final List<ByteString> HTTP_2_PROHIBITED_HEADERS = Util.immutableList(
- ByteString.encodeUtf8("connection"),
- ByteString.encodeUtf8("host"),
- ByteString.encodeUtf8("keep-alive"),
- ByteString.encodeUtf8("proxy-connection"),
- ByteString.encodeUtf8("te"),
- ByteString.encodeUtf8("transfer-encoding"),
- ByteString.encodeUtf8("encoding"),
- ByteString.encodeUtf8("upgrade"));
-
- private final HttpEngine httpEngine;
+ private static final List<ByteString> HTTP_2_SKIPPED_REQUEST_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TE,
+ TRANSFER_ENCODING,
+ ENCODING,
+ UPGRADE,
+ TARGET_METHOD,
+ TARGET_PATH,
+ TARGET_SCHEME,
+ TARGET_AUTHORITY,
+ TARGET_HOST,
+ VERSION);
+ private static final List<ByteString> HTTP_2_SKIPPED_RESPONSE_HEADERS = Util.immutableList(
+ CONNECTION,
+ HOST,
+ KEEP_ALIVE,
+ PROXY_CONNECTION,
+ TE,
+ TRANSFER_ENCODING,
+ ENCODING,
+ UPGRADE);
+
+ private final StreamAllocation streamAllocation;
private final FramedConnection framedConnection;
+ private HttpEngine httpEngine;
private FramedStream stream;
- public FramedTransport(HttpEngine httpEngine, FramedConnection framedConnection) {
- this.httpEngine = httpEngine;
+ public Http2xStream(StreamAllocation streamAllocation, FramedConnection framedConnection) {
+ this.streamAllocation = streamAllocation;
this.framedConnection = framedConnection;
}
+ @Override public void setHttpEngine(HttpEngine httpEngine) {
+ this.httpEngine = httpEngine;
+ }
+
@Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
return stream.getSink();
}
@@ -83,13 +127,14 @@ public final class FramedTransport implements Transport {
if (stream != null) return;
httpEngine.writingRequestHeaders();
- boolean permitsRequestBody = httpEngine.permitsRequestBody();
+ boolean permitsRequestBody = httpEngine.permitsRequestBody(request);
+ List<Header> requestHeaders = framedConnection.getProtocol() == Protocol.HTTP_2
+ ? http2HeadersList(request)
+ : spdy3HeadersList(request);
boolean hasResponseBody = true;
- String version = RequestLine.version(httpEngine.getConnection().getProtocol());
- stream = framedConnection.newStream(
- writeNameValueBlock(request, framedConnection.getProtocol(), version), permitsRequestBody,
- hasResponseBody);
+ stream = framedConnection.newStream(requestHeaders, permitsRequestBody, hasResponseBody);
stream.readTimeout().timeout(httpEngine.client.getReadTimeout(), TimeUnit.MILLISECONDS);
+ stream.writeTimeout().timeout(httpEngine.client.getWriteTimeout(), TimeUnit.MILLISECONDS);
}
@Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
@@ -101,7 +146,9 @@ public final class FramedTransport implements Transport {
}
@Override public Response.Builder readResponseHeaders() throws IOException {
- return readNameValueBlock(stream.getResponseHeaders(), framedConnection.getProtocol());
+ return framedConnection.getProtocol() == Protocol.HTTP_2
+ ? readHttp2HeadersList(stream.getResponseHeaders())
+ : readSpdy3HeadersList(stream.getResponseHeaders());
}
/**
@@ -109,43 +156,25 @@ public final class FramedTransport implements Transport {
* Names are all lowercase. No names are repeated. If any name has multiple
* values, they are concatenated using "\0" as a delimiter.
*/
- public static List<Header> writeNameValueBlock(Request request, Protocol protocol,
- String version) {
+ public static List<Header> spdy3HeadersList(Request request) {
Headers headers = request.headers();
- List<Header> result = new ArrayList<>(headers.size() + 10);
+ List<Header> result = new ArrayList<>(headers.size() + 5);
result.add(new Header(TARGET_METHOD, request.method()));
result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
- String host = Util.hostHeader(request.httpUrl());
- if (Protocol.SPDY_3 == protocol) {
- result.add(new Header(VERSION, version));
- result.add(new Header(TARGET_HOST, host));
- } else if (Protocol.HTTP_2 == protocol) {
- result.add(new Header(TARGET_AUTHORITY, host)); // Optional in HTTP/2
- } else {
- throw new AssertionError();
- }
+ result.add(new Header(VERSION, "HTTP/1.1"));
+ result.add(new Header(TARGET_HOST, Util.hostHeader(request.httpUrl())));
result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
- Set<ByteString> names = new LinkedHashSet<ByteString>();
+ Set<ByteString> names = new LinkedHashSet<>();
for (int i = 0, size = headers.size(); i < size; i++) {
// header names must be lowercase.
ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
- String value = headers.value(i);
// Drop headers that are forbidden when layering HTTP over SPDY.
- if (isProhibitedHeader(protocol, name)) continue;
-
- // They shouldn't be set, but if they are, drop them. We've already written them!
- if (name.equals(TARGET_METHOD)
- || name.equals(TARGET_PATH)
- || name.equals(TARGET_SCHEME)
- || name.equals(TARGET_AUTHORITY)
- || name.equals(TARGET_HOST)
- || name.equals(VERSION)) {
- continue;
- }
+ if (SPDY_3_SKIPPED_REQUEST_HEADERS.contains(name)) continue;
// If we haven't seen this name before, add the pair to the end of the list...
+ String value = headers.value(i);
if (names.add(name)) {
result.add(new Header(name, value));
continue;
@@ -167,16 +196,32 @@ public final class FramedTransport implements Transport {
return new StringBuilder(first).append('\0').append(second).toString();
}
+ public static List<Header> http2HeadersList(Request request) {
+ Headers headers = request.headers();
+ List<Header> result = new ArrayList<>(headers.size() + 4);
+ result.add(new Header(TARGET_METHOD, request.method()));
+ result.add(new Header(TARGET_PATH, RequestLine.requestPath(request.httpUrl())));
+ result.add(new Header(TARGET_AUTHORITY, Util.hostHeader(request.httpUrl()))); // Optional.
+ result.add(new Header(TARGET_SCHEME, request.httpUrl().scheme()));
+
+ for (int i = 0, size = headers.size(); i < size; i++) {
+ // header names must be lowercase.
+ ByteString name = ByteString.encodeUtf8(headers.name(i).toLowerCase(Locale.US));
+ if (!HTTP_2_SKIPPED_REQUEST_HEADERS.contains(name)) {
+ result.add(new Header(name, headers.value(i)));
+ }
+ }
+ return result;
+ }
+
/** Returns headers for a name value block containing a SPDY response. */
- public static Response.Builder readNameValueBlock(List<Header> headerBlock,
- Protocol protocol) throws IOException {
+ public static Response.Builder readSpdy3HeadersList(List<Header> headerBlock) throws IOException {
String status = null;
- String version = "HTTP/1.1"; // :version present only in spdy/3.
-
+ String version = "HTTP/1.1";
Headers.Builder headersBuilder = new Headers.Builder();
- headersBuilder.set(OkHeaders.SELECTED_PROTOCOL, protocol.toString());
for (int i = 0, size = headerBlock.size(); i < size; i++) {
ByteString name = headerBlock.get(i).name;
+
String values = headerBlock.get(i).value.utf8();
for (int start = 0; start < values.length(); ) {
int end = values.indexOf('\0', start);
@@ -188,7 +233,7 @@ public final class FramedTransport implements Transport {
status = value;
} else if (name.equals(VERSION)) {
version = value;
- } else if (!isProhibitedHeader(protocol, name)) { // Don't write forbidden headers!
+ } else if (!SPDY_3_SKIPPED_RESPONSE_HEADERS.contains(name)) {
headersBuilder.add(name.utf8(), value);
}
start = end + 1;
@@ -198,35 +243,54 @@ public final class FramedTransport implements Transport {
StatusLine statusLine = StatusLine.parse(version + " " + status);
return new Response.Builder()
- .protocol(protocol)
+ .protocol(Protocol.SPDY_3)
.code(statusLine.code)
.message(statusLine.message)
.headers(headersBuilder.build());
}
- @Override public ResponseBody openResponseBody(Response response) throws IOException {
- return new RealResponseBody(response.headers(), Okio.buffer(stream.getSource()));
- }
+ /** Returns headers for a name value block containing an HTTP/2 response. */
+ public static Response.Builder readHttp2HeadersList(List<Header> headerBlock) throws IOException {
+ String status = null;
+
+ Headers.Builder headersBuilder = new Headers.Builder();
+ for (int i = 0, size = headerBlock.size(); i < size; i++) {
+ ByteString name = headerBlock.get(i).name;
- @Override public void releaseConnectionOnIdle() {
+ String value = headerBlock.get(i).value.utf8();
+ if (name.equals(RESPONSE_STATUS)) {
+ status = value;
+ } else if (!HTTP_2_SKIPPED_RESPONSE_HEADERS.contains(name)) {
+ headersBuilder.add(name.utf8(), value);
+ }
+ }
+ if (status == null) throw new ProtocolException("Expected ':status' header not present");
+
+ StatusLine statusLine = StatusLine.parse("HTTP/1.1 " + status);
+ return new Response.Builder()
+ .protocol(Protocol.HTTP_2)
+ .code(statusLine.code)
+ .message(statusLine.message)
+ .headers(headersBuilder.build());
}
- @Override public void disconnect(HttpEngine engine) throws IOException {
- if (stream != null) stream.close(ErrorCode.CANCEL);
+ @Override public ResponseBody openResponseBody(Response response) throws IOException {
+ Source source = new StreamFinishingSource(stream.getSource());
+ return new RealResponseBody(response.headers(), Okio.buffer(source));
}
- @Override public boolean canReuseConnection() {
- return true; // TODO: framedConnection.isClosed() ?
+ @Override public void cancel() {
+ if (stream != null) stream.closeLater(ErrorCode.CANCEL);
}
- /** When true, this header should not be emitted or consumed. */
- private static boolean isProhibitedHeader(Protocol protocol, ByteString name) {
- if (protocol == Protocol.SPDY_3) {
- return SPDY_3_PROHIBITED_HEADERS.contains(name);
- } else if (protocol == Protocol.HTTP_2) {
- return HTTP_2_PROHIBITED_HEADERS.contains(name);
- } else {
- throw new AssertionError(protocol);
+ class StreamFinishingSource extends ForwardingSource {
+ public StreamFinishingSource(Source delegate) {
+ super(delegate);
+ }
+
+ @Override public void close() throws IOException {
+ streamAllocation.streamFinished(Http2xStream.this);
+ super.close();
}
}
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index 70eeaaa..b80f7d5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
@@ -20,7 +20,6 @@ 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.HttpUrl;
import com.squareup.okhttp.Interceptor;
@@ -36,18 +35,13 @@ 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.InterruptedIOException;
import java.net.CookieHandler;
import java.net.ProtocolException;
import java.net.Proxy;
-import java.net.SocketTimeoutException;
-import java.security.cert.CertificateException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HostnameVerifier;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSocketFactory;
import okio.Buffer;
import okio.BufferedSink;
@@ -111,13 +105,9 @@ public final class HttpEngine {
final OkHttpClient client;
- private Connection connection;
- private Address address;
- private RouteSelector routeSelector;
- private Route route;
+ public final StreamAllocation streamAllocation;
private final Response priorResponse;
-
- private Transport transport;
+ private HttpStream httpStream;
/** The time when the request headers were written, or -1 if they haven't been written yet. */
long sentRequestMillis = -1;
@@ -178,30 +168,20 @@ public final class HttpEngine {
* @param callerWritesRequestBody true for the {@code HttpURLConnection}-style interaction
* model where control flow is returned to the calling application to write the request body
* before the response body is readable.
- * @param connection the connection used for an intermediate response immediately prior to this
- * request/response pair, such as a same-host redirect. This engine assumes ownership of the
- * connection and must release it when it is unneeded.
- * @param routeSelector the route selector used for a failed attempt immediately preceding this
*/
public HttpEngine(OkHttpClient client, Request request, boolean bufferRequestBody,
- boolean callerWritesRequestBody, boolean forWebSocket, Connection connection,
- RouteSelector routeSelector, RetryableSink requestBodyOut, Response priorResponse) {
+ boolean callerWritesRequestBody, boolean forWebSocket, StreamAllocation streamAllocation,
+ RetryableSink requestBodyOut, Response priorResponse) {
this.client = client;
this.userRequest = request;
this.bufferRequestBody = bufferRequestBody;
this.callerWritesRequestBody = callerWritesRequestBody;
this.forWebSocket = forWebSocket;
- this.connection = connection;
- this.routeSelector = routeSelector;
+ this.streamAllocation = streamAllocation != null
+ ? streamAllocation
+ : new StreamAllocation(client.getConnectionPool(), createAddress(client, request));
this.requestBodyOut = requestBodyOut;
this.priorResponse = priorResponse;
-
- if (connection != null) {
- Internal.instance.setOwner(connection, this);
- this.route = connection.getRoute();
- } else {
- this.route = null;
- }
}
/**
@@ -218,7 +198,7 @@ public final class HttpEngine {
*/
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
- if (transport != null) throw new IllegalStateException();
+ if (httpStream != null) throw new IllegalStateException();
Request request = networkRequest(userRequest);
@@ -241,18 +221,14 @@ public final class HttpEngine {
}
if (networkRequest != null) {
- // Open a connection unless we inherited one from a redirect.
- if (connection == null) {
- connect();
- }
-
- transport = Internal.instance.newTransport(connection, this);
+ httpStream = connect();
+ httpStream.setHttpEngine(this);
// If the caller's control flow writes the request body, we need to create that stream
// immediately. And that means we need to immediately write the request headers, so we can
// start streaming the request body. (We may already have a request body if we're retrying a
// failed POST.)
- if (callerWritesRequestBody && permitsRequestBody() && requestBodyOut == null) {
+ if (callerWritesRequestBody && permitsRequestBody(networkRequest) && requestBodyOut == null) {
long contentLength = OkHeaders.contentLength(request);
if (bufferRequestBody) {
if (contentLength > Integer.MAX_VALUE) {
@@ -262,7 +238,7 @@ public final class HttpEngine {
if (contentLength != -1) {
// Buffer a request body of a known length.
- transport.writeRequestHeaders(networkRequest);
+ httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = new RetryableSink((int) contentLength);
} else {
// Buffer a request body of an unknown length. Don't write request
@@ -271,18 +247,12 @@ public final class HttpEngine {
requestBodyOut = new RetryableSink();
}
} else {
- transport.writeRequestHeaders(networkRequest);
- requestBodyOut = transport.createRequestBody(networkRequest, contentLength);
+ httpStream.writeRequestHeaders(networkRequest);
+ requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
}
}
} else {
- // We aren't using the network. Recycle a connection we may have inherited from a redirect.
- if (connection != null) {
- Internal.instance.recycle(client.getConnectionPool(), connection);
- connection = null;
- }
-
if (cacheResponse != null) {
// We have a valid cached response. Promote it to the user response immediately.
this.userResponse = cacheResponse.newBuilder()
@@ -306,48 +276,19 @@ public final class HttpEngine {
}
}
+ private HttpStream connect() throws RouteException, RequestException, IOException {
+ boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET");
+ return streamAllocation.newStream(client.getConnectTimeout(),
+ client.getReadTimeout(), client.getWriteTimeout(),
+ client.getRetryOnConnectionFailure(), doExtensiveHealthChecks);
+ }
+
private static Response stripBody(Response response) {
return response != null && response.body() != null
? response.newBuilder().body(null).build()
: response;
}
- /** Connect to the origin server either directly or via a proxy. */
- private void connect() throws RequestException, RouteException {
- if (connection != null) throw new IllegalStateException();
-
- if (routeSelector == null) {
- address = createAddress(client, networkRequest);
- try {
- routeSelector = RouteSelector.get(address, networkRequest, client);
- } catch (IOException e) {
- throw new RequestException(e);
- }
- }
-
- connection = createNextConnection();
- Internal.instance.connectAndSetOwner(client, connection, this, networkRequest);
- route = connection.getRoute();
- }
-
- private Connection createNextConnection() throws RouteException {
- ConnectionPool pool = client.getConnectionPool();
-
- // Always prefer pooled connections over new connections.
- for (Connection pooled; (pooled = pool.get(address)) != null; ) {
- if (networkRequest.method().equals("GET") || Internal.instance.isReadable(pooled)) {
- return pooled;
- }
- closeQuietly(pooled.getSocket());
- }
-
- try {
- Route route = routeSelector.next();
- return new Connection(pool, route);
- } catch (IOException e) {
- throw new RouteException(e);
- }
- }
/**
* Called immediately before the transport transmits HTTP request headers.
@@ -358,8 +299,8 @@ public final class HttpEngine {
sentRequestMillis = System.currentTimeMillis();
}
- boolean permitsRequestBody() {
- return HttpMethod.permitsRequestBody(userRequest.method());
+ boolean permitsRequestBody(Request request) {
+ return HttpMethod.permitsRequestBody(request.method());
}
/** Returns the request body or null if this request doesn't have a body. */
@@ -393,7 +334,7 @@ public final class HttpEngine {
}
public Connection getConnection() {
- return connection;
+ return streamAllocation.connection();
}
/**
@@ -402,64 +343,19 @@ public final class HttpEngine {
* there are no more routes to try.
*/
public HttpEngine recover(RouteException e) {
- if (routeSelector != null && connection != null) {
- connectFailed(routeSelector, e.getLastConnectException());
+ if (!streamAllocation.recover(e)) {
+ return null;
}
- if (routeSelector == null && connection == null // No connection.
- || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
- || !isRecoverable(e)) {
+ if (!client.getRetryOnConnectionFailure()) {
return null;
}
- Connection connection = close();
+ StreamAllocation streamAllocation = close();
// For failure recovery, use the same route selector with a new connection.
return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody,
- forWebSocket, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse);
- }
-
- private boolean isRecoverable(RouteException e) {
- // If the application has opted-out of recovery, don't recover.
- if (!client.getRetryOnConnectionFailure()) {
- return false;
- }
-
- // Problems with a route may mean the connection can be retried with a new route, or may
- // indicate a client-side or server-side issue that should not be retried. To tell, we must look
- // at the cause.
-
- IOException ioe = e.getLastConnectException();
-
- // If there was a protocol problem, don't recover.
- if (ioe instanceof ProtocolException) {
- return false;
- }
-
- // If there was an interruption don't recover, but if there was a timeout
- // we should try the next route (if there is one).
- if (ioe instanceof InterruptedIOException) {
- return ioe instanceof SocketTimeoutException;
- }
-
- // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
- // again with a different route.
- if (ioe instanceof SSLHandshakeException) {
- // If the problem was a CertificateException from the X509TrustManager,
- // do not retry.
- if (ioe.getCause() instanceof CertificateException) {
- return false;
- }
- }
- if (ioe instanceof SSLPeerUnverifiedException) {
- // e.g. a certificate pinning error.
- return false;
- }
-
- // An example of one we might want to retry with a different route is a problem connecting to a
- // proxy and would manifest as a standard IOException. Unless it is one we know we should not
- // retry, we return true and try a new route.
- return true;
+ forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
}
/**
@@ -469,63 +365,25 @@ public final class HttpEngine {
* body is buffered.
*/
public HttpEngine recover(IOException e, Sink requestBodyOut) {
- if (routeSelector != null && connection != null) {
- connectFailed(routeSelector, e);
+ if (!streamAllocation.recover(e, requestBodyOut)) {
+ return null;
}
- boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
- if (routeSelector == null && connection == null // No connection.
- || routeSelector != null && !routeSelector.hasNext() // No more routes to attempt.
- || !isRecoverable(e)
- || !canRetryRequestBody) {
+ if (!client.getRetryOnConnectionFailure()) {
return null;
}
- Connection connection = close();
+ StreamAllocation streamAllocation = close();
// For failure recovery, use the same route selector with a new connection.
return new HttpEngine(client, userRequest, bufferRequestBody, callerWritesRequestBody,
- forWebSocket, connection, routeSelector, (RetryableSink) requestBodyOut, priorResponse);
- }
-
- private void connectFailed(RouteSelector routeSelector, IOException e) {
- // If this is a recycled connection, don't count its failure against the route.
- if (Internal.instance.recycleCount(connection) > 0) return;
- Route failedRoute = connection.getRoute();
- routeSelector.connectFailed(failedRoute, e);
+ forWebSocket, streamAllocation, (RetryableSink) requestBodyOut, priorResponse);
}
public HttpEngine recover(IOException e) {
return recover(e, requestBodyOut);
}
- private boolean isRecoverable(IOException e) {
- // If the application has opted-out of recovery, don't recover.
- if (!client.getRetryOnConnectionFailure()) {
- return false;
- }
-
- // If there was a protocol problem, don't recover.
- if (e instanceof ProtocolException) {
- return false;
- }
-
- // If there was an interruption or timeout, don't recover.
- if (e instanceof InterruptedIOException) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Returns the route used to retrieve the response. Null if we haven't
- * connected yet, or if no connection was necessary.
- */
- public Route getRoute() {
- return route;
- }
-
private void maybeCache() throws IOException {
InternalCache responseCache = Internal.instance.internalCache(client);
if (responseCache == null) return;
@@ -551,11 +409,8 @@ public final class HttpEngine {
* either exhausted or closed. If it is unneeded when this is called, it will
* be released immediately.
*/
- public void releaseConnection() throws IOException {
- if (transport != null && connection != null) {
- transport.releaseConnectionOnIdle();
- }
- connection = null;
+ public void releaseStreamAllocation() throws IOException {
+ streamAllocation.release();
}
/**
@@ -567,25 +422,15 @@ public final class HttpEngine {
* transport layer connection has been established (such as a HTTP/2 stream) that is terminated.
* Otherwise if a socket connection is being established, that is terminated.
*/
- public void disconnect() {
- try {
- if (transport != null) {
- transport.disconnect(this);
- } else {
- Connection connection = this.connection;
- if (connection != null) {
- Internal.instance.closeIfOwnedBy(connection, this);
- }
- }
- } catch (IOException ignored) {
- }
+ public void cancel() {
+ streamAllocation.cancel();
}
/**
- * Release any resources held by this engine. If a connection is still held by
- * this engine, it is returned.
+ * Release any resources held by this engine. Returns the stream allocation held by this engine,
+ * which itself must be used or released.
*/
- public Connection close() {
+ public StreamAllocation close() {
if (bufferedRequestBody != null) {
// This also closes the wrapped requestBodyOut.
closeQuietly(bufferedRequestBody);
@@ -593,31 +438,14 @@ public final class HttpEngine {
closeQuietly(requestBodyOut);
}
- // If this engine never achieved a response body, its connection cannot be reused.
- if (userResponse == null) {
- if (connection != null) closeQuietly(connection.getSocket()); // TODO: does this break SPDY?
- connection = null;
- return null;
- }
-
- // Close the response body. This will recycle the connection if it is eligible.
- closeQuietly(userResponse.body());
-
- // Close the connection if it cannot be reused.
- if (transport != null && connection != null && !transport.canReuseConnection()) {
- closeQuietly(connection.getSocket());
- connection = null;
- return null;
- }
-
- // Prevent this engine from disconnecting a connection it no longer owns.
- if (connection != null && !Internal.instance.clearOwner(connection)) {
- connection = null;
+ if (userResponse != null) {
+ closeQuietly(userResponse.body());
+ } else {
+ // If this engine never achieved a response body, its stream allocation is dead.
+ streamAllocation.connectionFailed();
}
- Connection result = connection;
- connection = null;
- return result;
+ return streamAllocation;
}
/**
@@ -694,8 +522,7 @@ public final class HttpEngine {
result.header("Host", Util.hostHeader(request.httpUrl()));
}
- if ((connection == null || connection.getProtocol() != Protocol.HTTP_1_0)
- && request.header("Connection") == null) {
+ if (request.header("Connection") == null) {
result.header("Connection", "Keep-Alive");
}
@@ -742,7 +569,7 @@ public final class HttpEngine {
Response networkResponse;
if (forWebSocket) {
- transport.writeRequestHeaders(networkRequest);
+ httpStream.writeRequestHeaders(networkRequest);
networkResponse = readNetworkResponse();
} else if (!callerWritesRequestBody) {
@@ -763,7 +590,7 @@ public final class HttpEngine {
.header("Content-Length", Long.toString(contentLength))
.build();
}
- transport.writeRequestHeaders(networkRequest);
+ httpStream.writeRequestHeaders(networkRequest);
}
// Write the request body to the socket.
@@ -775,7 +602,7 @@ public final class HttpEngine {
requestBodyOut.close();
}
if (requestBodyOut instanceof RetryableSink) {
- transport.writeRequestBody((RetryableSink) requestBodyOut);
+ httpStream.writeRequestBody((RetryableSink) requestBodyOut);
}
}
@@ -795,7 +622,7 @@ public final class HttpEngine {
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
- releaseConnection();
+ releaseStreamAllocation();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
@@ -833,7 +660,7 @@ public final class HttpEngine {
}
@Override public Connection connection() {
- return connection;
+ return streamAllocation.connection();
}
@Override public Request request() {
@@ -872,17 +699,21 @@ public final class HttpEngine {
throw new IllegalStateException("network interceptor " + interceptor
+ " must call proceed() exactly once");
}
+ if (interceptedResponse == null) {
+ throw new NullPointerException("network interceptor " + interceptor
+ + " returned null");
+ }
return interceptedResponse;
}
- transport.writeRequestHeaders(request);
+ httpStream.writeRequestHeaders(request);
//Update the networkRequest with the possibly updated interceptor request.
networkRequest = request;
- if (permitsRequestBody() && request.body() != null) {
- Sink requestBodyOut = transport.createRequestBody(request, request.body().contentLength());
+ if (permitsRequestBody(request) && request.body() != null) {
+ Sink requestBodyOut = httpStream.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
@@ -901,22 +732,26 @@ public final class HttpEngine {
}
private Response readNetworkResponse() throws IOException {
- transport.finishRequest();
+ httpStream.finishRequest();
- Response networkResponse = transport.readResponseHeaders()
+ Response networkResponse = httpStream.readResponseHeaders()
.request(networkRequest)
- .handshake(connection.getHandshake())
+ .handshake(streamAllocation.connection().getHandshake())
.header(OkHeaders.SENT_MILLIS, Long.toString(sentRequestMillis))
.header(OkHeaders.RECEIVED_MILLIS, Long.toString(System.currentTimeMillis()))
.build();
if (!forWebSocket) {
networkResponse = networkResponse.newBuilder()
- .body(transport.openResponseBody(networkResponse))
+ .body(httpStream.openResponseBody(networkResponse))
.build();
}
- Internal.instance.setProtocol(connection, networkResponse.protocol());
+ if ("close".equalsIgnoreCase(networkResponse.request().header("Connection"))
+ || "close".equalsIgnoreCase(networkResponse.header("Connection"))) {
+ streamAllocation.noNewStreams();
+ }
+
return networkResponse;
}
@@ -969,7 +804,7 @@ public final class HttpEngine {
@Override public void close() throws IOException {
if (!cacheRequestClosed
- && !Util.discard(this, Transport.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
+ && !Util.discard(this, HttpStream.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)) {
cacheRequestClosed = true;
cacheRequest.abort();
}
@@ -1051,11 +886,16 @@ public final class HttpEngine {
*/
public Request followUpRequest() throws IOException {
if (userResponse == null) throw new IllegalStateException();
- Proxy selectedProxy = getRoute() != null
- ? getRoute().getProxy()
+ Connection connection = streamAllocation.connection();
+ Route route = connection != null
+ ? connection.getRoute()
+ : null;
+ Proxy selectedProxy = route != null
+ ? route.getProxy()
: client.getProxy();
int responseCode = userResponse.code();
+ final String method = userRequest.method();
switch (responseCode) {
case HTTP_PROXY_AUTH:
if (selectedProxy.type() != Proxy.Type.HTTP) {
@@ -1069,7 +909,7 @@ public final class HttpEngine {
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
- if (!userRequest.method().equals("GET") && !userRequest.method().equals("HEAD")) {
+ if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
@@ -1093,8 +933,12 @@ public final class HttpEngine {
// Redirects don't include a request body.
Request.Builder requestBuilder = userRequest.newBuilder();
- if (HttpMethod.permitsRequestBody(userRequest.method())) {
- requestBuilder.method("GET", null);
+ if (HttpMethod.permitsRequestBody(method)) {
+ if (HttpMethod.redirectsToGet(method)) {
+ requestBuilder.method("GET", null);
+ } else {
+ requestBuilder.method(method, null);
+ }
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
@@ -1135,7 +979,7 @@ public final class HttpEngine {
certificatePinner = client.getCertificatePinner();
}
- return new Address(request.httpUrl().host(), request.httpUrl().port(),
+ return new Address(request.httpUrl().host(), request.httpUrl().port(), client.getDns(),
client.getSocketFactory(), sslSocketFactory, hostnameVerifier, certificatePinner,
client.getAuthenticator(), client.getProxy(), client.getProtocols(),
client.getConnectionSpecs(), client.getProxySelector());
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
index b5f2a48..b6bf700 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpMethod.java
@@ -20,18 +20,30 @@ public final class HttpMethod {
return method.equals("POST")
|| method.equals("PATCH")
|| method.equals("PUT")
- || method.equals("DELETE");
+ || method.equals("DELETE")
+ || method.equals("MOVE"); // WebDAV
}
public static boolean requiresRequestBody(String method) {
return method.equals("POST")
|| method.equals("PUT")
- || method.equals("PATCH");
+ || method.equals("PATCH")
+ || method.equals("PROPPATCH") // WebDAV
+ || method.equals("REPORT"); // CalDAV/CardDAV (defined in WebDAV Versioning)
}
public static boolean permitsRequestBody(String method) {
return requiresRequestBody(method)
- || method.equals("DELETE"); // Permitted as spec is ambiguous.
+ || method.equals("OPTIONS")
+ || method.equals("DELETE") // Permitted as spec is ambiguous.
+ || method.equals("PROPFIND") // (WebDAV) without body: request <allprop/>
+ || method.equals("MKCOL") // (WebDAV) may contain a body, but behaviour is unspecified
+ || method.equals("LOCK"); // (WebDAV) body: create lock, without body: refresh lock
+ }
+
+ public static boolean redirectsToGet(String method) {
+ // All requests but PROPFIND should redirect to a GET request.
+ return !method.equals("PROPFIND");
}
private HttpMethod() {
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
index 77f7c9e..ef1deb7 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/Transport.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpStream.java
@@ -22,7 +22,7 @@ import com.squareup.okhttp.ResponseBody;
import java.io.IOException;
import okio.Sink;
-public interface Transport {
+public interface HttpStream {
/**
* The timeout to use while discarding a stream of input data. Since this is
* used for connection reuse, this timeout should be significantly less than
@@ -51,17 +51,11 @@ public interface Transport {
/** Returns a stream that reads the response body. */
ResponseBody openResponseBody(Response response) throws IOException;
- /**
- * Configures the response body to pool or close the socket connection when
- * the response body is closed.
- */
- void releaseConnectionOnIdle() throws IOException;
-
- void disconnect(HttpEngine engine) throws IOException;
+ void setHttpEngine(HttpEngine httpEngine);
/**
- * Returns true if the socket connection held by this transport can be reused
- * for a follow-up exchange.
+ * Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
+ * That may happen later by the connection pool thread.
*/
- boolean canReuseConnection();
+ void cancel();
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
deleted file mode 100644
index d02e1e5..0000000
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpTransport.java
+++ /dev/null
@@ -1,137 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.squareup.okhttp.internal.http;
-
-import com.squareup.okhttp.Request;
-import com.squareup.okhttp.Response;
-import com.squareup.okhttp.ResponseBody;
-import java.io.IOException;
-import okio.Okio;
-import okio.Sink;
-import okio.Source;
-
-public final class HttpTransport implements Transport {
- private final HttpEngine httpEngine;
- private final HttpConnection httpConnection;
-
- public HttpTransport(HttpEngine httpEngine, HttpConnection httpConnection) {
- this.httpEngine = httpEngine;
- this.httpConnection = httpConnection;
- }
-
- @Override public Sink createRequestBody(Request request, long contentLength) throws IOException {
- if ("chunked".equalsIgnoreCase(request.header("Transfer-Encoding"))) {
- // Stream a request body of unknown length.
- return httpConnection.newChunkedSink();
- }
-
- if (contentLength != -1) {
- // Stream a request body of a known length.
- return httpConnection.newFixedLengthSink(contentLength);
- }
-
- throw new IllegalStateException(
- "Cannot stream a request body without chunked encoding or a known content length!");
- }
-
- @Override public void finishRequest() throws IOException {
- httpConnection.flush();
- }
-
- @Override public void writeRequestBody(RetryableSink requestBody) throws IOException {
- httpConnection.writeRequestBody(requestBody);
- }
-
- /**
- * Prepares the HTTP headers and sends them to the server.
- *
- * <p>For streaming requests with a body, headers must be prepared
- * <strong>before</strong> the output stream has been written to. Otherwise
- * the body would need to be buffered!
- *
- * <p>For non-streaming requests with a body, headers must be prepared
- * <strong>after</strong> the output stream has been written to and closed.
- * This ensures that the {@code Content-Length} header field receives the
- * proper value.
- */
- public void writeRequestHeaders(Request request) throws IOException {
- httpEngine.writingRequestHeaders();
- String requestLine = RequestLine.get(request,
- httpEngine.getConnection().getRoute().getProxy().type(),
- httpEngine.getConnection().getProtocol());
- httpConnection.writeRequest(request.headers(), requestLine);
- }
-
- @Override public Response.Builder readResponseHeaders() throws IOException {
- return httpConnection.readResponse();
- }
-
- @Override public void releaseConnectionOnIdle() throws IOException {
- if (canReuseConnection()) {
- httpConnection.poolOnIdle();
- } else {
- httpConnection.closeOnIdle();
- }
- }
-
- @Override public boolean canReuseConnection() {
- // If the request specified that the connection shouldn't be reused, don't reuse it.
- if ("close".equalsIgnoreCase(httpEngine.getRequest().header("Connection"))) {
- return false;
- }
-
- // If the response specified that the connection shouldn't be reused, don't reuse it.
- if ("close".equalsIgnoreCase(httpEngine.getResponse().header("Connection"))) {
- return false;
- }
-
- if (httpConnection.isClosed()) {
- return false;
- }
-
- return true;
- }
-
- @Override public ResponseBody openResponseBody(Response response) throws IOException {
- Source source = getTransferStream(response);
- return new RealResponseBody(response.headers(), Okio.buffer(source));
- }
-
- private Source getTransferStream(Response response) throws IOException {
- if (!HttpEngine.hasBody(response)) {
- return httpConnection.newFixedLengthSource(0);
- }
-
- if ("chunked".equalsIgnoreCase(response.header("Transfer-Encoding"))) {
- return httpConnection.newChunkedSource(httpEngine);
- }
-
- long contentLength = OkHeaders.contentLength(response);
- if (contentLength != -1) {
- return httpConnection.newFixedLengthSource(contentLength);
- }
-
- // Wrap the input stream from the connection (rather than just returning
- // "socketIn" directly here), so that we can control its use after the
- // reference escapes.
- return httpConnection.newUnknownLengthSource();
- }
-
- @Override public void disconnect(HttpEngine engine) throws IOException {
- httpConnection.closeIfOwnedBy(engine);
- }
-}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
index c381c47..5e41c85 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/OkHeaders.java
@@ -55,6 +55,9 @@ public final class OkHeaders {
*/
public static final String SELECTED_PROTOCOL = PREFIX + "-Selected-Protocol";
+ /** Synthetic response header: the location from which the response was loaded. */
+ public static final String RESPONSE_SOURCE = PREFIX + "-Response-Source";
+
private OkHeaders() {
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
index d22be27..1a621a5 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RequestLine.java
@@ -1,7 +1,6 @@
package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.HttpUrl;
-import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import java.net.HttpURLConnection;
import java.net.Proxy;
@@ -15,7 +14,7 @@ public final class RequestLine {
* to the application by {@link HttpURLConnection#getHeaderFields}, so it
* needs to be set even if the transport is SPDY.
*/
- static String get(Request request, Proxy.Type proxyType, Protocol protocol) {
+ static String get(Request request, Proxy.Type proxyType) {
StringBuilder result = new StringBuilder();
result.append(request.method());
result.append(' ');
@@ -26,8 +25,7 @@ public final class RequestLine {
result.append(requestPath(request.httpUrl()));
}
- result.append(' ');
- result.append(version(protocol));
+ result.append(" HTTP/1.1");
return result.toString();
}
@@ -49,8 +47,4 @@ public final class RequestLine {
String query = url.encodedQuery();
return query != null ? (path + '?' + query) : path;
}
-
- public static String version(Protocol protocol) {
- return protocol == Protocol.HTTP_1_0 ? "HTTP/1.0" : "HTTP/1.1";
- }
}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
index b16bab3..3365914 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java
@@ -17,11 +17,7 @@ package com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.HttpUrl;
-import com.squareup.okhttp.OkHttpClient;
-import com.squareup.okhttp.Request;
import com.squareup.okhttp.Route;
-import com.squareup.okhttp.internal.Internal;
-import com.squareup.okhttp.internal.Network;
import com.squareup.okhttp.internal.RouteDatabase;
import java.io.IOException;
import java.net.InetAddress;
@@ -41,9 +37,6 @@ import java.util.NoSuchElementException;
*/
public final class RouteSelector {
private final Address address;
- private final HttpUrl url;
- private final Network network;
- private final OkHttpClient client;
private final RouteDatabase routeDatabase;
/* The most recently attempted route. */
@@ -61,19 +54,11 @@ public final class RouteSelector {
/* State for negotiating failed routes */
private final List<Route> postponedRoutes = new ArrayList<>();
- private RouteSelector(Address address, HttpUrl url, OkHttpClient client) {
+ public RouteSelector(Address address, RouteDatabase routeDatabase) {
this.address = address;
- this.url = url;
- this.client = client;
- this.routeDatabase = Internal.instance.routeDatabase(client);
- this.network = Internal.instance.network(client);
+ this.routeDatabase = routeDatabase;
- resetNextProxy(url, address.getProxy());
- }
-
- public static RouteSelector get(Address address, Request request, OkHttpClient client)
- throws IOException {
- return new RouteSelector(address, request.httpUrl(), client);
+ resetNextProxy(address.url(), address.getProxy());
}
/**
@@ -117,7 +102,7 @@ public final class RouteSelector {
if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && address.getProxySelector() != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
address.getProxySelector().connectFailed(
- url.uri(), failedRoute.getProxy().address(), failure);
+ address.url().uri(), failedRoute.getProxy().address(), failure);
}
routeDatabase.failed(failedRoute);
@@ -132,7 +117,7 @@ public final class RouteSelector {
// Try each of the ProxySelector choices until one connection succeeds. If none succeed
// then we'll try a direct connection below.
proxies = new ArrayList<>();
- List<Proxy> selectedProxies = client.getProxySelector().select(url.uri());
+ List<Proxy> selectedProxies = address.getProxySelector().select(url.uri());
if (selectedProxies != null) proxies.addAll(selectedProxies);
// Finally try a direct connection. We only try it once!
proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));
@@ -183,9 +168,15 @@ public final class RouteSelector {
+ "; port is out of range");
}
- // Try each address for best behavior in mixed IPv4/IPv6 environments.
- for (InetAddress inetAddress : network.resolveInetAddresses(socketHost)) {
- inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+ if (proxy.type() == Proxy.Type.SOCKS) {
+ inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
+ } else {
+ // Try each address for best behavior in mixed IPv4/IPv6 environments.
+ List<InetAddress> addresses = address.getDns().lookup(socketHost);
+ for (int i = 0, size = addresses.size(); i < size; i++) {
+ InetAddress inetAddress = addresses.get(i);
+ inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
+ }
}
nextInetSocketAddressIndex = 0;
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java
new file mode 100644
index 0000000..7d95338
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/http/StreamAllocation.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2015 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.http;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.ConnectionPool;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.Internal;
+import com.squareup.okhttp.internal.RouteDatabase;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.io.RealConnection;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.net.ProtocolException;
+import java.net.SocketTimeoutException;
+import java.security.cert.CertificateException;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import okio.Sink;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+/**
+ * This class coordinates the relationship between three entities:
+ *
+ * <ul>
+ * <li><strong>Connections:</strong> physical socket connections to remote servers. These are
+ * potentially slow to establish so it is necessary to be able to cancel a connection
+ * currently being connected.
+ * <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
+ * connections. Each connection has its own allocation limit, which defines how many
+ * concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
+ * at a time, SPDY and HTTP/2 typically carry multiple.
+ * <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
+ * its follow up requests. We prefer to keep all streams of a single call on the same
+ * connection for better behavior and locality.
+ * </ul>
+ *
+ * <p>Instances of this class act on behalf of the call, using one or more streams over one or
+ * more connections. This class has APIs to release each of the above resources:
+ *
+ * <ul>
+ * <li>{@link #noNewStreams()} prevents the connection from being used for new streams in the
+ * future. Use this after a {@code Connection: close} header, or when the connection may be
+ * inconsistent.
+ * <li>{@link #streamFinished streamFinished()} releases the active stream from this allocation.
+ * Note that only one stream may be active at a given time, so it is necessary to call {@link
+ * #streamFinished streamFinished()} before creating a subsequent stream with {@link
+ * #newStream newStream()}.
+ * <li>{@link #release()} removes the call's hold on the connection. Note that this won't
+ * immediately free the connection if there is a stream still lingering. That happens when a
+ * call is complete but its response body has yet to be fully consumed.
+ * </ul>
+ *
+ * <p>This class supports {@linkplain #cancel asynchronous canceling}. This is intended to have
+ * the smallest blast radius possible. If an HTTP/2 stream is active, canceling will cancel that
+ * stream but not the other streams sharing its connection. But if the TLS handshake is still in
+ * progress then canceling may break the entire connection.
+ */
+public final class StreamAllocation {
+ public final Address address;
+ private final ConnectionPool connectionPool;
+
+ // State guarded by connectionPool.
+ private RouteSelector routeSelector;
+ private RealConnection connection;
+ private boolean released;
+ private boolean canceled;
+ private HttpStream stream;
+
+ public StreamAllocation(ConnectionPool connectionPool, Address address) {
+ this.connectionPool = connectionPool;
+ this.address = address;
+ }
+
+ public HttpStream newStream(int connectTimeout, int readTimeout, int writeTimeout,
+ boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
+ throws RouteException, IOException {
+ try {
+ RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
+ writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
+
+ HttpStream resultStream;
+ if (resultConnection.framedConnection != null) {
+ resultStream = new Http2xStream(this, resultConnection.framedConnection);
+ } else {
+ resultConnection.getSocket().setSoTimeout(readTimeout);
+ resultConnection.source.timeout().timeout(readTimeout, MILLISECONDS);
+ resultConnection.sink.timeout().timeout(writeTimeout, MILLISECONDS);
+ resultStream = new Http1xStream(this, resultConnection.source, resultConnection.sink);
+ }
+
+ synchronized (connectionPool) {
+ resultConnection.streamCount++;
+ stream = resultStream;
+ return resultStream;
+ }
+ } catch (IOException e) {
+ throw new RouteException(e);
+ }
+ }
+
+ /**
+ * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
+ * until a healthy connection is found.
+ */
+ private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
+ int writeTimeout, boolean connectionRetryEnabled, boolean doExtensiveHealthChecks)
+ throws IOException, RouteException {
+ while (true) {
+ RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
+ connectionRetryEnabled);
+
+ // If this is a brand new connection, we can skip the extensive health checks.
+ synchronized (connectionPool) {
+ if (candidate.streamCount == 0) {
+ return candidate;
+ }
+ }
+
+ // Otherwise do a potentially-slow check to confirm that the pooled connection is still good.
+ if (candidate.isHealthy(doExtensiveHealthChecks)) {
+ return candidate;
+ }
+
+ connectionFailed();
+ }
+ }
+
+ /**
+ * Returns a connection to host a new stream. This prefers the existing connection if it exists,
+ * then the pool, finally building a new connection.
+ */
+ private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
+ boolean connectionRetryEnabled) throws IOException, RouteException {
+ synchronized (connectionPool) {
+ if (released) throw new IllegalStateException("released");
+ if (stream != null) throw new IllegalStateException("stream != null");
+ if (canceled) throw new IOException("Canceled");
+
+ RealConnection allocatedConnection = this.connection;
+ if (allocatedConnection != null && !allocatedConnection.noNewStreams) {
+ return allocatedConnection;
+ }
+
+ // Attempt to get a connection from the pool.
+ RealConnection pooledConnection = Internal.instance.get(connectionPool, address, this);
+ if (pooledConnection != null) {
+ this.connection = pooledConnection;
+ return pooledConnection;
+ }
+
+ // Attempt to create a connection.
+ if (routeSelector == null) {
+ routeSelector = new RouteSelector(address, routeDatabase());
+ }
+ }
+
+ Route route = routeSelector.next();
+ RealConnection newConnection = new RealConnection(route);
+ acquire(newConnection);
+
+ synchronized (connectionPool) {
+ Internal.instance.put(connectionPool, newConnection);
+ this.connection = newConnection;
+ if (canceled) throw new IOException("Canceled");
+ }
+
+ newConnection.connect(connectTimeout, readTimeout, writeTimeout, address.getConnectionSpecs(),
+ connectionRetryEnabled);
+ routeDatabase().connected(newConnection.getRoute());
+
+ return newConnection;
+ }
+
+ public void streamFinished(HttpStream stream) {
+ synchronized (connectionPool) {
+ if (stream == null || stream != this.stream) {
+ throw new IllegalStateException("expected " + this.stream + " but was " + stream);
+ }
+ }
+ deallocate(false, false, true);
+ }
+
+ public HttpStream stream() {
+ synchronized (connectionPool) {
+ return stream;
+ }
+ }
+
+ private RouteDatabase routeDatabase() {
+ return Internal.instance.routeDatabase(connectionPool);
+ }
+
+ public synchronized RealConnection connection() {
+ return connection;
+ }
+
+ public void release() {
+ deallocate(false, true, false);
+ }
+
+ /** Forbid new streams from being created on the connection that hosts this allocation. */
+ public void noNewStreams() {
+ deallocate(true, false, false);
+ }
+
+ /**
+ * Releases resources held by this allocation. If sufficient resources are allocated, the
+ * connection will be detached or closed.
+ */
+ private void deallocate(boolean noNewStreams, boolean released, boolean streamFinished) {
+ RealConnection connectionToClose = null;
+ synchronized (connectionPool) {
+ if (streamFinished) {
+ this.stream = null;
+ }
+ if (released) {
+ this.released = true;
+ }
+ if (connection != null) {
+ if (noNewStreams) {
+ connection.noNewStreams = true;
+ }
+ if (this.stream == null && (this.released || connection.noNewStreams)) {
+ release(connection);
+ if (connection.streamCount > 0) {
+ routeSelector = null;
+ }
+ if (connection.allocations.isEmpty()) {
+ connection.idleAtNanos = System.nanoTime();
+ if (Internal.instance.connectionBecameIdle(connectionPool, connection)) {
+ connectionToClose = connection;
+ }
+ }
+ connection = null;
+ }
+ }
+ }
+ if (connectionToClose != null) {
+ Util.closeQuietly(connectionToClose.getSocket());
+ }
+ }
+
+ public void cancel() {
+ HttpStream streamToCancel;
+ RealConnection connectionToCancel;
+ synchronized (connectionPool) {
+ canceled = true;
+ streamToCancel = stream;
+ connectionToCancel = connection;
+ }
+ if (streamToCancel != null) {
+ streamToCancel.cancel();
+ } else if (connectionToCancel != null) {
+ connectionToCancel.cancel();
+ }
+ }
+
+ private void connectionFailed(IOException e) {
+ synchronized (connectionPool) {
+ if (routeSelector != null) {
+ if (connection.streamCount == 0) {
+ // Record the failure on a fresh route.
+ Route failedRoute = connection.getRoute();
+ routeSelector.connectFailed(failedRoute, e);
+ } else {
+ // We saw a failure on a recycled connection, reset this allocation with a fresh route.
+ routeSelector = null;
+ }
+ }
+ }
+ connectionFailed();
+ }
+
+ /** Finish the current stream and prevent new streams from being created. */
+ public void connectionFailed() {
+ deallocate(true, false, true);
+ }
+
+ /**
+ * Use this allocation to hold {@code connection}. Each call to this must be paired with a call to
+ * {@link #release} on the same connection.
+ */
+ public void acquire(RealConnection connection) {
+ connection.allocations.add(new WeakReference<>(this));
+ }
+
+ /** Remove this allocation from the connection's list of allocations. */
+ private void release(RealConnection connection) {
+ for (int i = 0, size = connection.allocations.size(); i < size; i++) {
+ Reference<StreamAllocation> reference = connection.allocations.get(i);
+ if (reference.get() == this) {
+ connection.allocations.remove(i);
+ return;
+ }
+ }
+ throw new IllegalStateException();
+ }
+
+ public boolean recover(RouteException e) {
+ if (connection != null) {
+ connectionFailed(e.getLastConnectException());
+ }
+
+ if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt.
+ || !isRecoverable(e)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean recover(IOException e, Sink requestBodyOut) {
+ if (connection != null) {
+ int streamCount = connection.streamCount;
+ connectionFailed(e);
+
+ if (streamCount == 1) {
+ // This isn't a recycled connection.
+ // TODO(jwilson): find a better way for this.
+ return false;
+ }
+ }
+
+ boolean canRetryRequestBody = requestBodyOut == null || requestBodyOut instanceof RetryableSink;
+ if ((routeSelector != null && !routeSelector.hasNext()) // No more routes to attempt.
+ || !isRecoverable(e)
+ || !canRetryRequestBody) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isRecoverable(IOException e) {
+ // If there was a protocol problem, don't recover.
+ if (e instanceof ProtocolException) {
+ return false;
+ }
+
+ // If there was an interruption or timeout, don't recover.
+ if (e instanceof InterruptedIOException) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean isRecoverable(RouteException e) {
+ // Problems with a route may mean the connection can be retried with a new route, or may
+ // indicate a client-side or server-side issue that should not be retried. To tell, we must look
+ // at the cause.
+
+ IOException ioe = e.getLastConnectException();
+
+ // If there was a protocol problem, don't recover.
+ if (ioe instanceof ProtocolException) {
+ return false;
+ }
+
+ // If there was an interruption don't recover, but if there was a timeout
+ // we should try the next route (if there is one).
+ if (ioe instanceof InterruptedIOException) {
+ return ioe instanceof SocketTimeoutException;
+ }
+
+ // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
+ // again with a different route.
+ if (ioe instanceof SSLHandshakeException) {
+ // If the problem was a CertificateException from the X509TrustManager,
+ // do not retry.
+ if (ioe.getCause() instanceof CertificateException) {
+ return false;
+ }
+ }
+ if (ioe instanceof SSLPeerUnverifiedException) {
+ // e.g. a certificate pinning error.
+ return false;
+ }
+
+ // An example of one we might want to retry with a different route is a problem connecting to a
+ // proxy and would manifest as a standard IOException. Unless it is one we know we should not
+ // retry, we return true and try a new route.
+ return true;
+ }
+
+ @Override public String toString() {
+ return address.toString();
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java b/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java
new file mode 100644
index 0000000..9ff53c1
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/io/RealConnection.java
@@ -0,0 +1,407 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.io;
+
+import com.squareup.okhttp.Address;
+import com.squareup.okhttp.CertificatePinner;
+import com.squareup.okhttp.Connection;
+import com.squareup.okhttp.ConnectionSpec;
+import com.squareup.okhttp.Handshake;
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.Protocol;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.Response;
+import com.squareup.okhttp.Route;
+import com.squareup.okhttp.internal.ConnectionSpecSelector;
+import com.squareup.okhttp.internal.Platform;
+import com.squareup.okhttp.internal.Util;
+import com.squareup.okhttp.internal.Version;
+import com.squareup.okhttp.internal.framed.FramedConnection;
+import com.squareup.okhttp.internal.http.Http1xStream;
+import com.squareup.okhttp.internal.http.OkHeaders;
+import com.squareup.okhttp.internal.http.RouteException;
+import com.squareup.okhttp.internal.http.StreamAllocation;
+import com.squareup.okhttp.internal.tls.CertificateChainCleaner;
+import com.squareup.okhttp.internal.tls.OkHostnameVerifier;
+import com.squareup.okhttp.internal.tls.TrustRootIndex;
+import java.io.IOException;
+import java.lang.ref.Reference;
+import java.net.ConnectException;
+import java.net.Proxy;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownServiceException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.X509TrustManager;
+import okio.BufferedSink;
+import okio.BufferedSource;
+import okio.Okio;
+import okio.Source;
+
+import static com.squareup.okhttp.internal.Util.closeQuietly;
+import static java.net.HttpURLConnection.HTTP_OK;
+import static java.net.HttpURLConnection.HTTP_PROXY_AUTH;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
+public final class RealConnection implements Connection {
+ private final Route route;
+
+ /** The low-level TCP socket. */
+ private Socket rawSocket;
+
+ /**
+ * The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
+ * {@link #rawSocket} itself if this connection does not use SSL.
+ */
+ public Socket socket;
+ private Handshake handshake;
+ private Protocol protocol;
+ public volatile FramedConnection framedConnection;
+ public int streamCount;
+ public BufferedSource source;
+ public BufferedSink sink;
+ public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
+ public boolean noNewStreams;
+ public long idleAtNanos = Long.MAX_VALUE;
+
+ public RealConnection(Route route) {
+ this.route = route;
+ }
+
+ public void connect(int connectTimeout, int readTimeout, int writeTimeout,
+ List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
+ if (protocol != null) throw new IllegalStateException("already connected");
+
+ RouteException routeException = null;
+ ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
+ Proxy proxy = route.getProxy();
+ Address address = route.getAddress();
+
+ if (route.getAddress().getSslSocketFactory() == null
+ && !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
+ throw new RouteException(new UnknownServiceException(
+ "CLEARTEXT communication not supported: " + connectionSpecs));
+ }
+
+ while (protocol == null) {
+ try {
+ rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
+ ? address.getSocketFactory().createSocket()
+ : new Socket(proxy);
+ connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
+ } catch (IOException e) {
+ Util.closeQuietly(socket);
+ Util.closeQuietly(rawSocket);
+ socket = null;
+ rawSocket = null;
+ source = null;
+ sink = null;
+ handshake = null;
+ protocol = null;
+
+ if (routeException == null) {
+ routeException = new RouteException(e);
+ } else {
+ routeException.addConnectException(e);
+ }
+
+ if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
+ throw routeException;
+ }
+ }
+ }
+ }
+
+ /** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
+ private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
+ ConnectionSpecSelector connectionSpecSelector) throws IOException {
+ rawSocket.setSoTimeout(readTimeout);
+ try {
+ Platform.get().connectSocket(rawSocket, route.getSocketAddress(), connectTimeout);
+ } catch (ConnectException e) {
+ throw new ConnectException("Failed to connect to " + route.getSocketAddress());
+ }
+ source = Okio.buffer(Okio.source(rawSocket));
+ sink = Okio.buffer(Okio.sink(rawSocket));
+
+ if (route.getAddress().getSslSocketFactory() != null) {
+ connectTls(readTimeout, writeTimeout, connectionSpecSelector);
+ } else {
+ protocol = Protocol.HTTP_1_1;
+ socket = rawSocket;
+ }
+
+ if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
+ socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
+
+ FramedConnection framedConnection = new FramedConnection.Builder(true)
+ .socket(socket, route.getAddress().url().host(), source, sink)
+ .protocol(protocol)
+ .build();
+ framedConnection.sendConnectionPreface();
+
+ // Only assign the framed connection once the preface has been sent successfully.
+ this.framedConnection = framedConnection;
+ }
+ }
+
+ private void connectTls(int readTimeout, int writeTimeout,
+ ConnectionSpecSelector connectionSpecSelector) throws IOException {
+ if (route.requiresTunnel()) {
+ createTunnel(readTimeout, writeTimeout);
+ }
+
+ Address address = route.getAddress();
+ SSLSocketFactory sslSocketFactory = address.getSslSocketFactory();
+ boolean success = false;
+ SSLSocket sslSocket = null;
+ try {
+ // Create the wrapper over the connected socket.
+ sslSocket = (SSLSocket) sslSocketFactory.createSocket(
+ rawSocket, address.getUriHost(), address.getUriPort(), true /* autoClose */);
+
+ // Configure the socket's ciphers, TLS versions, and extensions.
+ ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
+ if (connectionSpec.supportsTlsExtensions()) {
+ Platform.get().configureTlsExtensions(
+ sslSocket, address.getUriHost(), address.getProtocols());
+ }
+
+ // Force handshake. This can throw!
+ sslSocket.startHandshake();
+ Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
+
+ // Verify that the socket's certificates are acceptable for the target host.
+ if (!address.getHostnameVerifier().verify(address.getUriHost(), sslSocket.getSession())) {
+ X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
+ throw new SSLPeerUnverifiedException("Hostname " + address.getUriHost() + " not verified:"
+ + "\n certificate: " + CertificatePinner.pin(cert)
+ + "\n DN: " + cert.getSubjectDN().getName()
+ + "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
+ }
+
+ // Check that the certificate pinner is satisfied by the certificates presented.
+ if (address.getCertificatePinner() != CertificatePinner.DEFAULT) {
+ TrustRootIndex trustRootIndex = trustRootIndex(address.getSslSocketFactory());
+ List<Certificate> certificates = new CertificateChainCleaner(trustRootIndex)
+ .clean(unverifiedHandshake.peerCertificates());
+ address.getCertificatePinner().check(address.getUriHost(), certificates);
+ }
+
+ // Success! Save the handshake and the ALPN protocol.
+ String maybeProtocol = connectionSpec.supportsTlsExtensions()
+ ? Platform.get().getSelectedProtocol(sslSocket)
+ : null;
+ socket = sslSocket;
+ source = Okio.buffer(Okio.source(socket));
+ sink = Okio.buffer(Okio.sink(socket));
+ handshake = unverifiedHandshake;
+ protocol = maybeProtocol != null
+ ? Protocol.get(maybeProtocol)
+ : Protocol.HTTP_1_1;
+ success = true;
+ } catch (AssertionError e) {
+ if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
+ throw e;
+ } finally {
+ if (sslSocket != null) {
+ Platform.get().afterHandshake(sslSocket);
+ }
+ if (!success) {
+ closeQuietly(sslSocket);
+ }
+ }
+ }
+
+ private static SSLSocketFactory lastSslSocketFactory;
+ private static TrustRootIndex lastTrustRootIndex;
+
+ /**
+ * Returns a trust root index for {@code sslSocketFactory}. This uses a static, single-element
+ * cache to avoid redoing reflection and SSL indexing in the common case where most SSL
+ * connections use the same SSL socket factory.
+ */
+ private static synchronized TrustRootIndex trustRootIndex(SSLSocketFactory sslSocketFactory) {
+ if (sslSocketFactory != lastSslSocketFactory) {
+ X509TrustManager trustManager = Platform.get().trustManager(sslSocketFactory);
+ lastTrustRootIndex = Platform.get().trustRootIndex(trustManager);
+ lastSslSocketFactory = sslSocketFactory;
+ }
+ return lastTrustRootIndex;
+ }
+
+ /**
+ * To make an HTTPS connection over an HTTP proxy, send an unencrypted
+ * CONNECT request to create the proxy connection. This may need to be
+ * retried if the proxy requires authorization.
+ */
+ private void createTunnel(int readTimeout, int writeTimeout) throws IOException {
+ // Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
+ Request tunnelRequest = createTunnelRequest();
+ HttpUrl url = tunnelRequest.httpUrl();
+ String requestLine = "CONNECT " + url.host() + ":" + url.port() + " HTTP/1.1";
+ while (true) {
+ Http1xStream tunnelConnection = new Http1xStream(null, source, sink);
+ source.timeout().timeout(readTimeout, MILLISECONDS);
+ sink.timeout().timeout(writeTimeout, MILLISECONDS);
+ tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
+ tunnelConnection.finishRequest();
+ Response response = tunnelConnection.readResponse().request(tunnelRequest).build();
+ // The response body from a CONNECT should be empty, but if it is not then we should consume
+ // it before proceeding.
+ long contentLength = OkHeaders.contentLength(response);
+ if (contentLength == -1L) {
+ contentLength = 0L;
+ }
+ Source body = tunnelConnection.newFixedLengthSource(contentLength);
+ Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
+ body.close();
+
+ switch (response.code()) {
+ case HTTP_OK:
+ // Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
+ // that happens, then we will have buffered bytes that are needed by the SSLSocket!
+ // This check is imperfect: it doesn't tell us whether a handshake will succeed, just
+ // that it will almost certainly fail because the proxy has sent unexpected data.
+ if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
+ throw new IOException("TLS tunnel buffered too many bytes!");
+ }
+ return;
+
+ case HTTP_PROXY_AUTH:
+ tunnelRequest = OkHeaders.processAuthHeader(
+ route.getAddress().getAuthenticator(), response, route.getProxy());
+ if (tunnelRequest != null) continue;
+ throw new IOException("Failed to authenticate with proxy");
+
+ default:
+ throw new IOException(
+ "Unexpected response code for CONNECT: " + response.code());
+ }
+ }
+ }
+
+ /**
+ * Returns a request that creates a TLS tunnel via an HTTP proxy, or null if
+ * no tunnel is necessary. Everything in the tunnel request is sent
+ * unencrypted to the proxy server, so tunnels include only the minimum set of
+ * headers. This avoids sending potentially sensitive data like HTTP cookies
+ * to the proxy unencrypted.
+ */
+ private Request createTunnelRequest() throws IOException {
+ return new Request.Builder()
+ .url(route.getAddress().url())
+ .header("Host", Util.hostHeader(route.getAddress().url()))
+ .header("Proxy-Connection", "Keep-Alive")
+ .header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid.
+ .build();
+ }
+
+ /** Returns true if {@link #connect} has been attempted on this connection. */
+ boolean isConnected() {
+ return protocol != null;
+ }
+
+ @Override public Route getRoute() {
+ return route;
+ }
+
+ public void cancel() {
+ // Close the raw socket so we don't end up doing synchronous I/O.
+ Util.closeQuietly(rawSocket);
+ }
+
+ @Override public Socket getSocket() {
+ return socket;
+ }
+
+ public int allocationLimit() {
+ FramedConnection framedConnection = this.framedConnection;
+ return framedConnection != null
+ ? framedConnection.maxConcurrentStreams()
+ : 1;
+ }
+
+ /** Returns true if this connection is ready to host new streams. */
+ public boolean isHealthy(boolean doExtensiveChecks) {
+ if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
+ return false;
+ }
+
+ if (framedConnection != null) {
+ return true; // TODO: check framedConnection.shutdown.
+ }
+
+ if (doExtensiveChecks) {
+ try {
+ int readTimeout = socket.getSoTimeout();
+ try {
+ socket.setSoTimeout(1);
+ if (source.exhausted()) {
+ return false; // Stream is exhausted; socket is closed.
+ }
+ return true;
+ } finally {
+ socket.setSoTimeout(readTimeout);
+ }
+ } catch (SocketTimeoutException ignored) {
+ // Read timed out; socket is good.
+ } catch (IOException e) {
+ return false; // Couldn't read; socket is closed.
+ }
+ }
+
+ return true;
+ }
+
+ @Override public Handshake getHandshake() {
+ return handshake;
+ }
+
+ /**
+ * Returns true if this is a SPDY connection. Such connections can be used
+ * in multiple HTTP requests simultaneously.
+ */
+ public boolean isMultiplexed() {
+ return framedConnection != null;
+ }
+
+ @Override public Protocol getProtocol() {
+ return protocol != null ? protocol : Protocol.HTTP_1_1;
+ }
+
+ @Override public String toString() {
+ return "Connection{"
+ + route.getAddress().url().host() + ":" + route.getAddress().url().port()
+ + ", proxy="
+ + route.getProxy()
+ + " hostAddress="
+ + route.getSocketAddress()
+ + " cipherSuite="
+ + (handshake != null ? handshake.cipherSuite() : "none")
+ + " protocol="
+ + protocol
+ + '}';
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java
new file mode 100644
index 0000000..0beba94
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/AndroidTrustRootIndex.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.tls;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509Certificate;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * A index of trusted root certificates that exploits knowledge of Android implementation details.
+ * This class is potentially much faster to initialize than {@link RealTrustRootIndex} because
+ * it doesn't need to load and index trusted CA certificates.
+ */
+public final class AndroidTrustRootIndex implements TrustRootIndex {
+ private final X509TrustManager trustManager;
+ private final Method findByIssuerAndSignatureMethod;
+
+ public AndroidTrustRootIndex(
+ X509TrustManager trustManager, Method findByIssuerAndSignatureMethod) {
+ this.findByIssuerAndSignatureMethod = findByIssuerAndSignatureMethod;
+ this.trustManager = trustManager;
+ }
+
+ @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) {
+ try {
+ TrustAnchor trustAnchor = (TrustAnchor) findByIssuerAndSignatureMethod.invoke(
+ trustManager, cert);
+ return trustAnchor != null
+ ? trustAnchor.getTrustedCert()
+ : null;
+ } catch (IllegalAccessException e) {
+ throw new AssertionError();
+ } catch (InvocationTargetException e) {
+ return null;
+ }
+ }
+
+ public static TrustRootIndex get(X509TrustManager trustManager) {
+ // From org.conscrypt.TrustManagerImpl, we want the method with this signature:
+ // private TrustAnchor findTrustAnchorByIssuerAndSignature(X509Certificate lastCert);
+ try {
+ Method method = trustManager.getClass().getDeclaredMethod(
+ "findTrustAnchorByIssuerAndSignature", X509Certificate.class);
+ method.setAccessible(true);
+ return new AndroidTrustRootIndex(trustManager, method);
+ } catch (NoSuchMethodException e) {
+ return null;
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java
new file mode 100644
index 0000000..0e53298
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/CertificateChainCleaner.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.tls;
+
+import java.security.GeneralSecurityException;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.List;
+import javax.net.ssl.SSLPeerUnverifiedException;
+
+/**
+ * Computes the effective certificate chain from the raw array returned by Java's built in TLS APIs.
+ * Cleaning a chain returns a list of certificates where the first element is {@code chain[0]}, each
+ * certificate is signed by the certificate that follows, and the last certificate is a trusted CA
+ * certificate.
+ *
+ * <p>Use of the chain cleaner is necessary to omit unexpected certificates that aren't relevant to
+ * the TLS handshake and to extract the trusted CA certificate for the benefit of certificate
+ * pinning.
+ *
+ * <p>This class includes code from <a href="https://conscrypt.org/">Conscrypt's</a> {@code
+ * TrustManagerImpl} and {@code TrustedCertificateIndex}.
+ */
+public final class CertificateChainCleaner {
+ /** The maximum number of signers in a chain. We use 9 for consistency with OpenSSL. */
+ private static final int MAX_SIGNERS = 9;
+
+ private final TrustRootIndex trustRootIndex;
+
+ public CertificateChainCleaner(TrustRootIndex trustRootIndex) {
+ this.trustRootIndex = trustRootIndex;
+ }
+
+ /**
+ * Returns a cleaned chain for {@code chain}.
+ *
+ * <p>This method throws if the complete chain to a trusted CA certificate cannot be constructed.
+ * This is unexpected unless the trust root index in this class has a different trust manager than
+ * what was used to establish {@code chain}.
+ */
+ public List<Certificate> clean(List<Certificate> chain) throws SSLPeerUnverifiedException {
+ Deque<Certificate> queue = new ArrayDeque<>(chain);
+ List<Certificate> result = new ArrayList<>();
+ result.add(queue.removeFirst());
+ boolean foundTrustedCertificate = false;
+
+ followIssuerChain:
+ for (int c = 0; c < MAX_SIGNERS; c++) {
+ X509Certificate toVerify = (X509Certificate) result.get(result.size() - 1);
+
+ // If this cert has been signed by a trusted cert, use that. Add the trusted certificate to
+ // the end of the chain unless it's already present. (That would happen if the first
+ // certificate in the chain is itself a self-signed and trusted CA certificate.)
+ X509Certificate trustedCert = trustRootIndex.findByIssuerAndSignature(toVerify);
+ if (trustedCert != null) {
+ if (result.size() > 1 || !toVerify.equals(trustedCert)) {
+ result.add(trustedCert);
+ }
+ if (verifySignature(trustedCert, trustedCert)) {
+ return result; // The self-signed cert is a root CA. We're done.
+ }
+ foundTrustedCertificate = true;
+ continue;
+ }
+
+ // Search for the certificate in the chain that signed this certificate. This is typically the
+ // next element in the chain, but it could be any element.
+ for (Iterator<Certificate> i = queue.iterator(); i.hasNext(); ) {
+ X509Certificate signingCert = (X509Certificate) i.next();
+ if (verifySignature(toVerify, signingCert)) {
+ i.remove();
+ result.add(signingCert);
+ continue followIssuerChain;
+ }
+ }
+
+ // We've reached the end of the chain. If any cert in the chain is trusted, we're done.
+ if (foundTrustedCertificate) {
+ return result;
+ }
+
+ // The last link isn't trusted. Fail.
+ throw new SSLPeerUnverifiedException("Failed to find a trusted cert that signed " + toVerify);
+ }
+
+ throw new SSLPeerUnverifiedException("Certificate chain too long: " + result);
+ }
+
+ /** Returns true if {@code toVerify} was signed by {@code signingCert}'s public key. */
+ private boolean verifySignature(X509Certificate toVerify, X509Certificate signingCert) {
+ if (!toVerify.getIssuerDN().equals(signingCert.getSubjectDN())) return false;
+ try {
+ toVerify.verify(signingCert.getPublicKey());
+ return true;
+ } catch (GeneralSecurityException verifyFailed) {
+ return false;
+ }
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java
new file mode 100644
index 0000000..885eea4
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/RealTrustRootIndex.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2016 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal.tls;
+
+import java.security.PublicKey;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import javax.security.auth.x500.X500Principal;
+
+public final class RealTrustRootIndex implements TrustRootIndex {
+ private final Map<X500Principal, List<X509Certificate>> subjectToCaCerts;
+
+ public RealTrustRootIndex(X509Certificate... caCerts) {
+ subjectToCaCerts = new LinkedHashMap<>();
+ for (X509Certificate caCert : caCerts) {
+ X500Principal subject = caCert.getSubjectX500Principal();
+ List<X509Certificate> subjectCaCerts = subjectToCaCerts.get(subject);
+ if (subjectCaCerts == null) {
+ subjectCaCerts = new ArrayList<>(1);
+ subjectToCaCerts.put(subject, subjectCaCerts);
+ }
+ subjectCaCerts.add(caCert);
+ }
+ }
+
+ @Override public X509Certificate findByIssuerAndSignature(X509Certificate cert) {
+ X500Principal issuer = cert.getIssuerX500Principal();
+ List<X509Certificate> subjectCaCerts = subjectToCaCerts.get(issuer);
+ if (subjectCaCerts == null) return null;
+
+ for (X509Certificate caCert : subjectCaCerts) {
+ PublicKey publicKey = caCert.getPublicKey();
+ try {
+ cert.verify(publicKey);
+ return caCert;
+ } catch (Exception ignored) {
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/android/main/java/com/squareup/okhttp/internal/Version.java b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
index 6a63f9b..6b0036b 100644
--- a/android/main/java/com/squareup/okhttp/internal/Version.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/tls/TrustRootIndex.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 Square, Inc.
+ * Copyright (C) 2016 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,14 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.squareup.okhttp.internal;
+package com.squareup.okhttp.internal.tls;
-public final class Version {
- public static String userAgent() {
- String agent = System.getProperty("http.agent");
- return agent != null ? agent : ("Java" + System.getProperty("java.version"));
- }
+import java.security.cert.X509Certificate;
- private Version() {
- }
+public interface TrustRootIndex {
+ /** Returns the trusted CA certificate that signed {@code cert}. */
+ X509Certificate findByIssuerAndSignature(X509Certificate cert);
}
diff --git a/okio/README.android b/okio/README.android
deleted file mode 100644
index a143b63..0000000
--- a/okio/README.android
+++ /dev/null
@@ -1,12 +0,0 @@
-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/okio/src/main/java/okio/DeflaterSink.java b/okio/okio/src/main/java/okio/DeflaterSink.java
index 3e325b2..a5bca15 100644
--- a/okio/okio/src/main/java/okio/DeflaterSink.java
+++ b/okio/okio/src/main/java/okio/DeflaterSink.java
@@ -17,6 +17,7 @@ package okio;
import java.io.IOException;
import java.util.zip.Deflater;
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import static okio.Util.checkOffsetAndCount;
@@ -79,9 +80,7 @@ public final class DeflaterSink implements Sink {
}
}
- // ANDROID-BEGIN
- // @IgnoreJRERequirement
- // ANDROID-END
+ @IgnoreJRERequirement
private void deflate(boolean syncFlush) throws IOException {
Buffer buffer = sink.buffer();
while (true) {
diff --git a/okio/okio/src/main/java/okio/Okio.java b/okio/okio/src/main/java/okio/Okio.java
index 7816050..7ba166e 100644
--- a/okio/okio/src/main/java/okio/Okio.java
+++ b/okio/okio/src/main/java/okio/Okio.java
@@ -25,8 +25,12 @@ import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
import java.util.logging.Level;
import java.util.logging.Logger;
+import org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement;
import static okio.Util.checkOffsetAndCount;
@@ -164,14 +168,12 @@ public final class Okio {
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 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));
+ }
/** Returns a sink that writes to {@code file}. */
public static Sink sink(File file) throws FileNotFoundException {
@@ -185,14 +187,12 @@ public final class Okio {
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 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));
+ }
/**
* Returns a source that reads from {@code socket}. Prefer this over {@link
diff --git a/okio/okio/src/test/java/okio/BufferedSinkTest.java b/okio/okio/src/test/java/okio/BufferedSinkTest.java
index f546214..05a7ccc 100644
--- a/okio/okio/src/test/java/okio/BufferedSinkTest.java
+++ b/okio/okio/src/test/java/okio/BufferedSinkTest.java
@@ -39,9 +39,7 @@ public class BufferedSinkTest {
BufferedSink create(Buffer data);
}
- // ANDROID-BEGIN
- // @Parameterized.Parameters(name = "{0}")
- // ANDROID-END
+ @Parameterized.Parameters(name = "{0}")
public static List<Object[]> parameters() {
return Arrays.asList(new Object[] {
new Factory() {
@@ -66,10 +64,8 @@ public class BufferedSinkTest {
});
}
- // ANDROID-BEGIN
- // @Parameterized.Parameter
- public Factory factory = (Factory) (parameters().get(0))[0];
- // ANDROID-END
+ @Parameterized.Parameter
+ public Factory factory;
private Buffer data;
private BufferedSink sink;
diff --git a/okio/okio/src/test/java/okio/BufferedSourceTest.java b/okio/okio/src/test/java/okio/BufferedSourceTest.java
index 767c071..6e42e15 100644
--- a/okio/okio/src/test/java/okio/BufferedSourceTest.java
+++ b/okio/okio/src/test/java/okio/BufferedSourceTest.java
@@ -92,9 +92,7 @@ public class BufferedSourceTest {
BufferedSource source;
}
- // ANDROID-BEGIN
- // @Parameterized.Parameters(name = "{0}")
- // ANDROID-END
+ @Parameterized.Parameters(name = "{0}")
public static List<Object[]> parameters() {
return Arrays.asList(
new Object[] { BUFFER_FACTORY },
@@ -102,10 +100,8 @@ public class BufferedSourceTest {
new Object[] { ONE_BYTE_AT_A_TIME_FACTORY });
}
- // ANDROID-BEGIN
- // @Parameterized.Parameter
- public Factory factory = (Factory) (parameters().get(0))[0];
- // ANDROID-END
+ @Parameterized.Parameter
+ public Factory factory;
private BufferedSink sink;
private BufferedSource source;
diff --git a/okio/okio/src/test/java/okio/OkioTest.java b/okio/okio/src/test/java/okio/OkioTest.java
index 815a51f..ec92db6 100644
--- a/okio/okio/src/test/java/okio/OkioTest.java
+++ b/okio/okio/src/test/java/okio/OkioTest.java
@@ -19,6 +19,8 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
@@ -65,21 +67,19 @@ public final class OkioTest {
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 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();
+ }
@Test public void sinkFromOutputStream() throws Exception {
Buffer data = new Buffer();
diff --git a/okio/okio/src/test/java/okio/ReadUtf8LineTest.java b/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
index 9867ca0..5ea2dca 100644
--- a/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
+++ b/okio/okio/src/test/java/okio/ReadUtf8LineTest.java
@@ -34,9 +34,7 @@ public final class ReadUtf8LineTest {
BufferedSource create(Buffer data);
}
- // ANDROID-BEGIN
- // @Parameterized.Parameters(name = "{0}")
- // ANDROID-END
+ @Parameterized.Parameters(name = "{0}")
public static List<Object[]> parameters() {
return Arrays.asList(
new Object[] { new Factory() {
@@ -73,10 +71,8 @@ public final class ReadUtf8LineTest {
);
}
- // ANDROID-BEGIN
- // @Parameterized.Parameter
- public Factory factory = (Factory) (parameters().get(0))[0];
- // ANDROID-END
+ @Parameterized.Parameter
+ public Factory factory;
private Buffer data;
private BufferedSource source;
diff --git a/pom.xml b/pom.xml
index 7219018..4654188 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
<packaging>pom</packaging>
<name>OkHttp (Parent)</name>
@@ -31,6 +31,8 @@
<module>okhttp-ws</module>
<module>okhttp-ws-tests</module>
+ <module>okhttp-logging-interceptor</module>
+
<module>okcurl</module>
<module>mockwebserver</module>
<module>samples</module>
@@ -52,6 +54,7 @@
<apache.http.version>4.2.2</apache.http.version>
<airlift.version>0.6</airlift.version>
<guava.version>16.0</guava.version>
+ <android.version>4.1.1.4</android.version>
<!-- Test Dependencies -->
<junit.version>4.11</junit.version>
@@ -61,7 +64,7 @@
<url>https://github.com/square/okhttp/</url>
<connection>scm:git:https://github.com/square/okhttp.git</connection>
<developerConnection>scm:git:git@github.com:square/okhttp.git</developerConnection>
- <tag>HEAD</tag>
+ <tag>parent-2.7.5</tag>
</scm>
<issueManagement>
@@ -113,6 +116,11 @@
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
+ <dependency>
+ <groupId>com.google.android</groupId>
+ <artifactId>android</artifactId>
+ <version>${android.version}</version>
+ </dependency>
</dependencies>
</dependencyManagement>
diff --git a/samples/crawler/pom.xml b/samples/crawler/pom.xml
index aebd0eb..e92d9ed 100644
--- a/samples/crawler/pom.xml
+++ b/samples/crawler/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>crawler</artifactId>
diff --git a/samples/guide/pom.xml b/samples/guide/pom.xml
index 55e2671..a72fd64 100644
--- a/samples/guide/pom.xml
+++ b/samples/guide/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>guide</artifactId>
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
index 8e5334a..2c6cfa0 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/PostMultipart.java
@@ -40,7 +40,7 @@ public final class PostMultipart {
RequestBody requestBody = new MultipartBuilder()
.type(MultipartBuilder.FORM)
.addFormDataPart("title", "Square Logo")
- .addFormDataPart("image", null,
+ .addFormDataPart("image", "logo-square.png",
RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
.build();
diff --git a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
index d439e99..877812e 100644
--- a/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
+++ b/samples/guide/src/main/java/com/squareup/okhttp/recipes/WebSocketEcho.java
@@ -2,7 +2,9 @@ package com.squareup.okhttp.recipes;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import com.squareup.okhttp.ws.WebSocket;
import com.squareup.okhttp.ws.WebSocketCall;
import com.squareup.okhttp.ws.WebSocketListener;
@@ -10,11 +12,10 @@ import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import okio.Buffer;
-import okio.BufferedSource;
+import okio.ByteString;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.BINARY;
-import static com.squareup.okhttp.ws.WebSocket.PayloadType.TEXT;
+import static com.squareup.okhttp.ws.WebSocket.BINARY;
+import static com.squareup.okhttp.ws.WebSocket.TEXT;
public final class WebSocketEcho implements WebSocketListener {
private final Executor writeExecutor = Executors.newSingleThreadExecutor();
@@ -35,9 +36,9 @@ public final class WebSocketEcho implements WebSocketListener {
writeExecutor.execute(new Runnable() {
@Override public void run() {
try {
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("Hello..."));
- webSocket.sendMessage(TEXT, new Buffer().writeUtf8("...World!"));
- webSocket.sendMessage(BINARY, new Buffer().writeInt(0xdeadbeef));
+ webSocket.sendMessage(RequestBody.create(TEXT, "Hello..."));
+ webSocket.sendMessage(RequestBody.create(TEXT, "...World!"));
+ webSocket.sendMessage(RequestBody.create(BINARY, ByteString.decodeHex("deadbeef")));
webSocket.close(1000, "Goodbye, World!");
} catch (IOException e) {
System.err.println("Unable to send messages: " + e.getMessage());
@@ -46,18 +47,13 @@ public final class WebSocketEcho implements WebSocketListener {
});
}
- @Override public void onMessage(BufferedSource payload, PayloadType type) throws IOException {
- switch (type) {
- case TEXT:
- System.out.println("MESSAGE: " + payload.readUtf8());
- break;
- case BINARY:
- System.out.println("MESSAGE: " + payload.readByteString().hex());
- break;
- default:
- throw new IllegalStateException("Unknown payload type: " + type);
+ @Override public void onMessage(ResponseBody message) throws IOException {
+ if (message.contentType() == TEXT) {
+ System.out.println("MESSAGE: " + message.string());
+ } else {
+ System.out.println("MESSAGE: " + message.source().readByteString().hex());
}
- payload.close();
+ message.close();
}
@Override public void onPong(Buffer payload) {
diff --git a/samples/pom.xml b/samples/pom.xml
index 29f1e87..77ca5f5 100644
--- a/samples/pom.xml
+++ b/samples/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp</groupId>
<artifactId>parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<groupId>com.squareup.okhttp.sample</groupId>
diff --git a/samples/simple-client/pom.xml b/samples/simple-client/pom.xml
index 3a1aa7d..0065b7b 100644
--- a/samples/simple-client/pom.xml
+++ b/samples/simple-client/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>simple-client</artifactId>
diff --git a/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java b/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
index e616d41..d897a9a 100644
--- a/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
+++ b/samples/simple-client/src/main/java/com/squareup/okhttp/sample/OkHttpContributors.java
@@ -5,6 +5,7 @@ import com.google.gson.reflect.TypeToken;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
+import com.squareup.okhttp.ResponseBody;
import java.io.Reader;
import java.util.Collections;
import java.util.Comparator;
@@ -34,8 +35,10 @@ public class OkHttpContributors {
Response response = client.newCall(request).execute();
// Deserialize HTTP response to concrete type.
- Reader body = response.body().charStream();
- List<Contributor> contributors = GSON.fromJson(body, CONTRIBUTORS.getType());
+ ResponseBody body = response.body();
+ Reader charStream = body.charStream();
+ List<Contributor> contributors = GSON.fromJson(charStream, CONTRIBUTORS.getType());
+ body.close();
// Sort list by the most contributions.
Collections.sort(contributors, new Comparator<Contributor>() {
diff --git a/samples/static-server/pom.xml b/samples/static-server/pom.xml
index f223151..75b492d 100644
--- a/samples/static-server/pom.xml
+++ b/samples/static-server/pom.xml
@@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.okhttp.sample</groupId>
<artifactId>sample-parent</artifactId>
- <version>2.6.0-SNAPSHOT</version>
+ <version>2.7.5</version>
</parent>
<artifactId>static-server</artifactId>
diff --git a/website/index.html b/website/index.html
index 86695a4..ecc7c69 100644
--- a/website/index.html
+++ b/website/index.html
@@ -155,7 +155,7 @@ limitations under the License.</pre>
</ul>
<ul class="nav nav-pills nav-stacked secondary">
<li><a href="https://github.com/square/okhttp/wiki">Wiki</a></li>
- <li><a href="javadoc/index.html">Javadoc</a></li>
+ <li><a href="2.x/okhttp/">Javadoc</a></li>
<li><a href="http://stackoverflow.com/questions/tagged/okhttp?sort=active">StackOverflow</a></li>
</ul>
</div>