aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeil Fuller <nfuller@google.com>2014-11-21 16:47:45 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2014-11-21 16:47:46 +0000
commit80a1db2828f1a1ae290a6cc8105616666fa97e73 (patch)
tree7de099975388df74f08974549ce1ec40670566c4
parentfcc9fffc119d3ecb6482c04875eec2e6e0378c88 (diff)
parentbcce0a3d26d66d33beb742ae2adddb3b7db5ad08 (diff)
downloadandroid_external_okhttp-80a1db2828f1a1ae290a6cc8105616666fa97e73.tar.gz
android_external_okhttp-80a1db2828f1a1ae290a6cc8105616666fa97e73.tar.bz2
android_external_okhttp-80a1db2828f1a1ae290a6cc8105616666fa97e73.zip
Merge "Changes for dealing with more granular TLS connection fallback"
-rw-r--r--android/main/java/com/squareup/okhttp/internal/Platform.java45
-rw-r--r--android/test/java/com/squareup/okhttp/internal/PlatformTest.java4
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java8
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java7
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java8
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java83
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java145
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java116
-rw-r--r--okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java97
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Connection.java145
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/Route.java16
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java22
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java124
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java123
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java1
-rw-r--r--okhttp/src/main/java/com/squareup/okhttp/internal/http/RouteSelector.java72
16 files changed, 721 insertions, 295 deletions
diff --git a/android/main/java/com/squareup/okhttp/internal/Platform.java b/android/main/java/com/squareup/okhttp/internal/Platform.java
index 121b156..1a398d4 100644
--- a/android/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/android/main/java/com/squareup/okhttp/internal/Platform.java
@@ -79,34 +79,33 @@ public final class Platform {
return url.toURILenient();
}
- public void enableTlsExtensions(SSLSocket socket, String uriHost) {
+ public void configureSecureSocket(SSLSocket socket, String uriHost, boolean isFallback) {
SET_USE_SESSION_TICKETS.invokeOptionalWithoutCheckedException(socket, true);
SET_HOSTNAME.invokeOptionalWithoutCheckedException(socket, uriHost);
- }
- public void supportTlsIntolerantServer(SSLSocket socket) {
- // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
- // the SCSV cipher is added to signal that a protocol fallback has taken place.
- final String fallbackScsv = "TLS_FALLBACK_SCSV";
- boolean socketSupportsFallbackScsv = false;
- String[] supportedCipherSuites = socket.getSupportedCipherSuites();
- for (int i = supportedCipherSuites.length - 1; i >= 0; i--) {
- String supportedCipherSuite = supportedCipherSuites[i];
- if (fallbackScsv.equals(supportedCipherSuite)) {
- socketSupportsFallbackScsv = true;
- break;
+ if (isFallback) {
+ // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
+ // the SCSV cipher is added to signal that a protocol fallback has taken place.
+ final String fallbackScsv = "TLS_FALLBACK_SCSV";
+ boolean socketSupportsFallbackScsv = false;
+ String[] supportedCipherSuites = socket.getSupportedCipherSuites();
+ for (int i = supportedCipherSuites.length - 1; i >= 0; i--) {
+ String supportedCipherSuite = supportedCipherSuites[i];
+ if (fallbackScsv.equals(supportedCipherSuite)) {
+ socketSupportsFallbackScsv = true;
+ break;
+ }
+ }
+ if (socketSupportsFallbackScsv) {
+ // Add the SCSV cipher to the set of enabled ciphers.
+ String[] enabledCipherSuites = socket.getEnabledCipherSuites();
+ String[] newEnabledCipherSuites = new String[enabledCipherSuites.length + 1];
+ System.arraycopy(enabledCipherSuites, 0,
+ newEnabledCipherSuites, 0, enabledCipherSuites.length);
+ newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
+ socket.setEnabledCipherSuites(newEnabledCipherSuites);
}
}
- if (socketSupportsFallbackScsv) {
- // Add the SCSV cipher to the set of enabled ciphers.
- String[] enabledCipherSuites = socket.getEnabledCipherSuites();
- String[] newEnabledCipherSuites = new String[enabledCipherSuites.length + 1];
- System.arraycopy(enabledCipherSuites, 0,
- newEnabledCipherSuites, 0, enabledCipherSuites.length);
- newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
- socket.setEnabledCipherSuites(newEnabledCipherSuites);
- }
- socket.setEnabledProtocols(new String[]{"SSLv3"});
}
/**
diff --git a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
index 9e293f6..9bb7f6f 100644
--- a/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
+++ b/android/test/java/com/squareup/okhttp/internal/PlatformTest.java
@@ -47,10 +47,10 @@ public class PlatformTest {
// Expect no error
TestSSLSocketImpl arbitrarySocketImpl = new TestSSLSocketImpl();
- platform.enableTlsExtensions(arbitrarySocketImpl, "host");
+ platform.configureSecureSocket(arbitrarySocketImpl, "host", false);
FullOpenSSLSocketImpl openSslSocket = new FullOpenSSLSocketImpl();
- platform.enableTlsExtensions(openSslSocket, "host");
+ platform.configureSecureSocket(openSslSocket, "host", false);
assertTrue(openSslSocket.useSessionTickets);
assertEquals("host", openSslSocket.hostname);
}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
index e41a7de..c0afe53 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/AsyncApiTest.java
@@ -117,18 +117,16 @@ public final class AsyncApiTest {
}
@Test public void recoverFromTlsHandshakeFailure() throws Exception {
- // Android now disables SSLv3 by default. To test fallback we re-enable it for the server. This
- // can be removed once OkHttp is updated to support other fallback protocols.
- SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory(
+ SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
sslContext.getSocketFactory(), "TLSv1", "SSLv3");
- server.useHttps(serverSocketFactory, false);
+ server.useHttps(socketFactory, false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setBody("abc"));
server.play();
final boolean disableTlsFallbackScsv = true;
SSLSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(sslContext.getSocketFactory(), disableTlsFallbackScsv);
+ new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
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 a2a2653..77ea831 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/ConnectionPoolTest.java
@@ -85,8 +85,8 @@ public final class ConnectionPoolTest {
spdySocketAddress = new InetSocketAddress(InetAddress.getByName(spdyServer.getHostName()),
spdyServer.getPort());
- Route httpRoute = new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true);
- Route spdyRoute = new Route(spdyAddress, Proxy.NO_PROXY, spdySocketAddress, true);
+ 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);
httpA = new Connection(pool, httpRoute);
httpA.connect(200, 200, null);
@@ -131,8 +131,7 @@ public final class ConnectionPoolTest {
Connection connection = pool.get(httpAddress);
assertNull(connection);
- connection = new Connection(
- pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress, true));
+ connection = new Connection(pool, new Route(httpAddress, Proxy.NO_PROXY, httpSocketAddress));
connection.connect(200, 200, null);
connection.setOwner(owner);
assertEquals(0, pool.getConnectionCount());
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
index 5253b37..48b680a 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/SyncApiTest.java
@@ -108,18 +108,16 @@ public final class SyncApiTest {
}
@Test public void recoverFromTlsHandshakeFailure() throws Exception {
- // Android now disables SSLv3 by default. To test fallback we re-enable it for the server. This
- // can be removed once OkHttp is updated to support other fallback protocols.
- SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory(
+ SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
sslContext.getSocketFactory(), "TLSv1", "SSLv3");
- server.useHttps(serverSocketFactory, false);
+ server.useHttps(socketFactory, false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setBody("abc"));
server.play();
final boolean disableTlsFallbackScsv = true;
SSLSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(sslContext.getSocketFactory(), disableTlsFallbackScsv);
+ new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java
new file mode 100644
index 0000000..9ca6bbc
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsConfigurationTest.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.squareup.okhttp.internal;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class TlsConfigurationTest {
+
+ private static final SSLContext sslContext = SslContextBuilder.localhost();
+
+ @Test
+ public void sslV3Only() throws Exception {
+ SSLSocket compatibleSocket = createSocketWithEnabledProtocols("SSLv3", "TLSv1");
+ try {
+ assertTrue(TlsConfiguration.SSL_V3_ONLY.isCompatible(compatibleSocket));
+ TlsConfiguration.SSL_V3_ONLY.configureProtocols(compatibleSocket);
+ assertEnabledProtocols(compatibleSocket, "SSLv3");
+ } finally {
+ compatibleSocket.close();
+ }
+
+ SSLSocket incompatibleSocket = createSocketWithEnabledProtocols("TLSv1");
+ try {
+ assertFalse(TlsConfiguration.SSL_V3_ONLY.isCompatible(incompatibleSocket));
+ } finally {
+ incompatibleSocket.close();
+ }
+
+ assertFalse(TlsConfiguration.SSL_V3_ONLY.supportsNpn());
+ }
+
+ @Test
+ public void useDefault() throws Exception {
+ String[] defaultProtocols = { "SSLv3", "TLSv1" };
+ SSLSocket socket = createSocketWithEnabledProtocols(defaultProtocols);
+
+ try {
+ assertTrue(TlsConfiguration.USE_DEFAULT.isCompatible(socket));
+ TlsConfiguration.USE_DEFAULT.configureProtocols(socket);
+ assertEnabledProtocols(socket, defaultProtocols);
+ } finally {
+ socket.close();
+ }
+
+ assertTrue(TlsConfiguration.USE_DEFAULT.supportsNpn());
+ }
+
+ private SSLSocket createSocketWithEnabledProtocols(String... protocols) throws IOException {
+ SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
+ socket.setEnabledProtocols(protocols);
+ return socket;
+ }
+
+ private static void assertEnabledProtocols(SSLSocket socket, String... required) {
+ Set<String> actual = new HashSet<String>(Arrays.asList(socket.getEnabledProtocols()));
+ Set<String> expected = new HashSet<String>(Arrays.asList(required));
+ assertEquals(expected, actual);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java
new file mode 100644
index 0000000..89fb09c
--- /dev/null
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/TlsFallbackStrategyTest.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocket;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class TlsFallbackStrategyTest {
+
+ private static final SSLContext sslContext = SslContextBuilder.localhost();
+ private static final String[] TLSV1_AND_SSLV3 = new String[] { "SSLv3", "TLSv1" };
+ private static final String[] TLSV1_ONLY = new String[] { "TLSv1" };
+ public static final SSLHandshakeException RETRYABLE_EXCEPTION = new SSLHandshakeException(
+ "Simulated handshake exception");
+
+ private TlsFallbackStrategy fallbackStrategy;
+ private Platform platform;
+
+ @Before
+ public void setUp() throws Exception {
+ fallbackStrategy = TlsFallbackStrategy.create();
+ platform = new Platform();
+ }
+
+ @Test
+ public void nonRetryableIOException() throws Exception {
+ SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV1_AND_SSLV3);
+ try {
+ fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
+
+ boolean retry = fallbackStrategy.connectionFailed(new IOException("Non-handshake exception"));
+ assertFalse(retry);
+ } finally {
+ supportsSslV3.close();
+ }
+ }
+
+ @Test
+ public void nonRetryableSSLHandshakeException() throws Exception {
+ SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV1_AND_SSLV3);
+ try {
+ fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
+
+ SSLHandshakeException trustIssueException =
+ new SSLHandshakeException("Certificate handshake exception",
+ new CertificateException());
+ boolean retry = fallbackStrategy.connectionFailed(trustIssueException);
+ assertFalse(retry);
+ } finally {
+ supportsSslV3.close();
+ }
+ }
+
+ @Test
+ public void retryableSSLHandshakeException() throws Exception {
+ SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV1_AND_SSLV3);
+ try {
+ fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
+
+ boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
+ assertTrue(retry);
+ } finally {
+ supportsSslV3.close();
+ }
+ }
+
+ @Test
+ public void allFallbacksSupported() throws Exception {
+ SSLSocket supportsSslV3 = createSocketWithEnabledProtocols(TLSV1_AND_SSLV3);
+ try {
+ fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
+ assertEnabledProtocols(supportsSslV3, TLSV1_AND_SSLV3);
+
+ boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
+ assertTrue(retry);
+ } finally {
+ supportsSslV3.close();
+ }
+
+ supportsSslV3 = createSocketWithEnabledProtocols(TLSV1_AND_SSLV3);
+ try {
+ fallbackStrategy.configureSecureSocket(supportsSslV3, "host", platform);
+ assertEnabledProtocols(supportsSslV3, "SSLv3");
+
+ boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
+ assertFalse(retry);
+ } finally {
+ supportsSslV3.close();
+ }
+ }
+
+ @Test
+ public void sslV3NotSupported() throws Exception {
+ SSLSocket socket = createSocketWithEnabledProtocols(TLSV1_ONLY);
+ try {
+ fallbackStrategy.configureSecureSocket(socket, "host", platform);
+
+ assertEnabledProtocols(socket, TLSV1_ONLY);
+
+ boolean retry = fallbackStrategy.connectionFailed(RETRYABLE_EXCEPTION);
+ assertFalse(retry);
+ } finally {
+ socket.close();
+ }
+ }
+
+ private SSLSocket createSocketWithEnabledProtocols(String... protocols) throws IOException {
+ SSLSocket socket = (SSLSocket) sslContext.getSocketFactory().createSocket();
+ socket.setEnabledProtocols(protocols);
+ return socket;
+ }
+
+ private static void assertEnabledProtocols(SSLSocket socket, String... required) {
+ Set<String> actual = new HashSet<String>(Arrays.asList(socket.getEnabledProtocols()));
+ Set<String> expected = new HashSet<String>(Arrays.asList(required));
+ assertEquals(expected, actual);
+ }
+}
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/RouteSelectorTest.java
index c8e2647..dd806c6 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
@@ -38,7 +38,6 @@ import java.util.NoSuchElementException;
import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Test;
@@ -91,8 +90,7 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -115,8 +113,7 @@ public final class RouteSelectorTest {
RouteDatabase routeDatabase = new RouteDatabase();
routeDatabase.failed(connection.getRoute());
routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns, routeDatabase);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
assertFalse(routeSelector.hasNext());
try {
routeSelector.next("GET");
@@ -133,10 +130,8 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort,
- false);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(proxyAHost);
@@ -151,10 +146,8 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(uri.getHost());
@@ -172,8 +165,7 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -187,10 +179,8 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort);
assertFalse(routeSelector.hasNext());
dns.assertRequests(uri.getHost());
@@ -210,24 +200,20 @@ public final class RouteSelectorTest {
// First try the IP addresses of the first proxy, in sequence.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort,
- false);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort);
dns.assertRequests(proxyAHost);
// Next try the IP address of the second proxy.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(254, 1);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort);
dns.assertRequests(proxyBHost);
// Finally try the only IP address of the origin server.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(253, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -245,8 +231,7 @@ public final class RouteSelectorTest {
// Only the origin server will be attempted.
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
@@ -265,8 +250,7 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
@@ -280,48 +264,17 @@ public final class RouteSelectorTest {
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(255, 1);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
dns.assertRequests(proxyAHost);
assertTrue(routeSelector.hasNext());
dns.inetAddresses = makeFakeAddresses(254, 1);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
dns.assertRequests(uriHost);
assertFalse(routeSelector.hasNext());
}
- // https://github.com/square/okhttp/issues/442
- @Test public void nonSslErrorAddsAllTlsModesToFailedRoute() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
- hostnameVerifier, authenticator, Proxy.NO_PROXY, protocols);
- RouteDatabase routeDatabase = new RouteDatabase();
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- routeDatabase);
-
- dns.inetAddresses = makeFakeAddresses(255, 1);
- Connection connection = routeSelector.next("GET");
- routeSelector.connectFailed(connection, new IOException("Non SSL exception"));
- assertTrue(routeDatabase.failedRoutesCount() == 2);
- assertFalse(routeSelector.hasNext());
- }
-
- @Test public void sslErrorAddsOnlyFailedTlsModeToFailedRoute() throws Exception {
- Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
- hostnameVerifier, authenticator, Proxy.NO_PROXY, protocols);
- RouteDatabase routeDatabase = new RouteDatabase();
- RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
- routeDatabase);
-
- dns.inetAddresses = makeFakeAddresses(255, 1);
- Connection connection = routeSelector.next("GET");
- routeSelector.connectFailed(connection, new SSLHandshakeException("SSL exception"));
- assertTrue(routeDatabase.failedRoutesCount() == 1);
- assertTrue(routeSelector.hasNext());
- }
-
@Test public void multipleProxiesMultipleInetAddressesMultipleTlsModes() throws Exception {
Address address = new Address(uriHost, uriPort, socketFactory, sslSocketFactory,
hostnameVerifier, authenticator, null, protocols);
@@ -332,39 +285,21 @@ public final class RouteSelectorTest {
// Proxy A
dns.inetAddresses = makeFakeAddresses(255, 2);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort,
- true);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort);
dns.assertRequests(proxyAHost);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[0], proxyAPort,
- false);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort,
- true);
- assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyA, dns.inetAddresses[1], proxyAPort);
// Proxy B
dns.inetAddresses = makeFakeAddresses(254, 2);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort,
- true);
+ assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort);
dns.assertRequests(proxyBHost);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[0], proxyBPort,
- false);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[1], proxyBPort,
- true);
- assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[1], proxyBPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, proxyB, dns.inetAddresses[1], proxyBPort);
// Origin
dns.inetAddresses = makeFakeAddresses(253, 2);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- true);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort);
dns.assertRequests(uriHost);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[0], uriPort,
- false);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort,
- true);
- assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort,
- false);
+ assertConnection(routeSelector.next("GET"), address, NO_PROXY, dns.inetAddresses[1], uriPort);
assertFalse(routeSelector.hasNext());
}
@@ -376,7 +311,7 @@ public final class RouteSelectorTest {
RouteDatabase routeDatabase = new RouteDatabase();
RouteSelector routeSelector = new RouteSelector(address, uri, proxySelector, pool, dns,
routeDatabase);
- dns.inetAddresses = makeFakeAddresses(255, 1);
+ dns.inetAddresses = makeFakeAddresses(255, 2);
// Extract the regular sequence of routes from selector.
List<Connection> regularRoutes = new ArrayList<Connection>();
@@ -402,12 +337,11 @@ public final class RouteSelectorTest {
}
private void assertConnection(Connection connection, Address address, Proxy proxy,
- InetAddress socketAddress, int socketPort, boolean modernTls) {
+ InetAddress socketAddress, int socketPort) {
assertEquals(address, connection.getRoute().getAddress());
assertEquals(proxy, connection.getRoute().getProxy());
assertEquals(socketAddress, connection.getRoute().getSocketAddress().getAddress());
assertEquals(socketPort, connection.getRoute().getSocketAddress().getPort());
- assertEquals(modernTls, connection.getRoute().isModernTls());
}
private static InetAddress[] makeFakeAddresses(int prefix, int count) {
diff --git a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
index 05d906b..4d0a00e 100644
--- a/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
+++ b/okhttp-tests/src/test/java/com/squareup/okhttp/internal/http/URLConnectionTest.java
@@ -107,7 +107,6 @@ public final class URLConnectionTest {
private MockWebServer server = new MockWebServer();
private MockWebServer server2 = new MockWebServer();
- private SSLSocketFactory fallbackServerSocketFactory;
private final OkHttpClient client = new OkHttpClient();
private HttpURLConnection connection;
@@ -117,11 +116,6 @@ public final class URLConnectionTest {
@Before public void setUp() throws Exception {
hostName = server.getHostName();
server.setNpnEnabled(false);
-
- // Android now disables SSLv3 by default. To test fallback we re-enable it for the server. This
- // can be removed once OkHttp is updated to support other fallback protocols.
- fallbackServerSocketFactory = new LimitedProtocolsSocketFactory(
- sslContext.getSocketFactory(), "TLSv1", "SSLv3");
}
@After public void tearDown() throws Exception {
@@ -620,15 +614,18 @@ public final class URLConnectionTest {
}
}
- @Test public void connectViaHttpsWithSSLFallback() throws IOException, InterruptedException {
- server.useHttps(fallbackServerSocketFactory, false);
+ @Test public void connectViaHttpsWithSSLFallback() throws Exception {
+ SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
+ sslContext.getSocketFactory(), "TLSv1", "SSLv3");
+
+ server.useHttps(socketFactory, false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.enqueue(new MockResponse().setBody("this response comes via SSL"));
server.play();
final boolean disableTlsFallbackScsv = true;
FallbackTestClientSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(sslContext.getSocketFactory(), disableTlsFallbackScsv);
+ new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
connection = client.open(server.getUrl("/foo"));
@@ -642,6 +639,69 @@ public final class URLConnectionTest {
assertEquals(2, clientSocketFactory.getCreatedSockets().size());
}
+ @Test public void connectViaHttpsWithSSLFallback_serverDoesNotSupportFallbackProtocol()
+ throws Exception {
+ SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory(
+ sslContext.getSocketFactory(), "TLSv1");
+ server.useHttps(serverSocketFactory, false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+ server.play();
+
+ final boolean disableTlsFallbackScsv = true;
+ FallbackTestClientSocketFactory clientSocketFactory =
+ new FallbackTestClientSocketFactory(
+ new LimitedProtocolsSocketFactory(sslContext.getSocketFactory(), "TLSv1", "SSLv3"),
+ disableTlsFallbackScsv);
+ client.setSslSocketFactory(clientSocketFactory);
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ connection = client.open(server.getUrl("/foo"));
+
+ try {
+ connection = client.open(server.getUrl("/foo"));
+ connection.getInputStream();
+ fail();
+ } catch (SSLHandshakeException expected) {
+ }
+
+ // The first request is handled by MockWebServer and intentionally failed.
+ assertEquals(1, server.getRequestCount());
+ // The client will attempt a fallback connection using SSLv3, but fail because the server does
+ // not support it.
+ assertEquals(2, clientSocketFactory.getCreatedSockets().size());
+ }
+
+ @Test public void connectViaHttpsWithSSLFallback_clientDoesNotSupportFallbackProtocol()
+ throws Exception {
+
+ SSLSocketFactory serverSocketFactory = new LimitedProtocolsSocketFactory(
+ sslContext.getSocketFactory(), "TLSv1", "SSLv3");
+ server.useHttps(serverSocketFactory, false);
+ server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
+ server.play();
+
+ final boolean disableTlsFallbackScsv = true;
+ FallbackTestClientSocketFactory clientSocketFactory =
+ new FallbackTestClientSocketFactory(
+ new LimitedProtocolsSocketFactory(sslContext.getSocketFactory(), "TLSv1"),
+ disableTlsFallbackScsv);
+ client.setSslSocketFactory(clientSocketFactory);
+ client.setHostnameVerifier(new RecordingHostnameVerifier());
+ connection = client.open(server.getUrl("/foo"));
+
+ try {
+ connection = client.open(server.getUrl("/foo"));
+ connection.getInputStream();
+ fail();
+ } catch (SSLHandshakeException expected) {
+ }
+
+ // The first request is handled by MockWebServer and intentionally failed.
+ assertEquals(1, server.getRequestCount());
+ // The client will not attempt a fallback connection if there is no support for the fallback
+ // protocol on the client.
+ assertEquals(1, clientSocketFactory.getCreatedSockets().size());
+ }
+
// After the introduction of the TLS_FALLBACK_SCSV we expect a failure if the initial
// handshake fails and the server supports TLS_FALLBACK_SCSV. MockWebServer on Android uses
// sockets that enforced TLS_FALLBACK_SCSV checks by default.
@@ -656,13 +716,16 @@ public final class URLConnectionTest {
return;
}
- server.useHttps(fallbackServerSocketFactory, false);
+ SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
+ sslContext.getSocketFactory(), "TLSv1", "SSLv3");
+
+ server.useHttps(socketFactory, false);
server.enqueue(new MockResponse().setSocketPolicy(SocketPolicy.FAIL_HANDSHAKE));
server.play();
final boolean disableTlsFallbackScsv = false;
FallbackTestClientSocketFactory clientSocketFactory =
- new FallbackTestClientSocketFactory(sslContext.getSocketFactory(), disableTlsFallbackScsv);
+ new FallbackTestClientSocketFactory(socketFactory, disableTlsFallbackScsv);
client.setSslSocketFactory(clientSocketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
try {
@@ -674,9 +737,9 @@ public final class URLConnectionTest {
// The first request is handled by MockWebServer and intentionally failed.
assertEquals(1, server.getRequestCount());
- // We assume there will be at least one fallback attempt. The number depends on the fallback
- // protocols attempted and the protocols available on the test platform.
- assertTrue(clientSocketFactory.getCreatedSockets().size() > 1);
+ // There will be one fallback attempt with the enabled client protocols. Though supported by the
+ // server it will fail because of the TLS_FALLBACK_SCSV check.
+ assertEquals(2, clientSocketFactory.getCreatedSockets().size());
}
/**
@@ -686,14 +749,16 @@ public final class URLConnectionTest {
* https://github.com/square/okhttp/issues/515
*/
@Test public void sslFallbackNotUsedWhenRecycledConnectionFails() throws Exception {
- server.useHttps(fallbackServerSocketFactory, false);
+ SSLSocketFactory socketFactory = new LimitedProtocolsSocketFactory(
+ sslContext.getSocketFactory(), "TLSv1", "SSLv3");
+ server.useHttps(socketFactory, false);
server.enqueue(new MockResponse()
.setBody("abc")
.setSocketPolicy(SocketPolicy.DISCONNECT_AT_END));
server.enqueue(new MockResponse().setBody("def"));
server.play();
- client.setSslSocketFactory(sslContext.getSocketFactory());
+ client.setSslSocketFactory(socketFactory);
client.setHostnameVerifier(new RecordingHostnameVerifier());
assertContent("abc", client.open(server.getUrl("/")));
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Connection.java b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
index 743c33b..d0cd18b 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Connection.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Connection.java
@@ -24,6 +24,8 @@ import com.squareup.okhttp.internal.http.HttpEngine;
import com.squareup.okhttp.internal.http.HttpTransport;
import com.squareup.okhttp.internal.http.OkHeaders;
import com.squareup.okhttp.internal.http.SpdyTransport;
+import com.squareup.okhttp.internal.TlsConfiguration;
+import com.squareup.okhttp.internal.TlsFallbackStrategy;
import com.squareup.okhttp.internal.spdy.SpdyConnection;
import java.io.Closeable;
import java.io.IOException;
@@ -34,6 +36,7 @@ import okio.ByteString;
import okio.OkBuffer;
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;
@@ -71,6 +74,7 @@ public final class Connection implements Closeable {
private boolean connected = false;
private HttpConnection httpConnection;
private SpdyConnection spdyConnection;
+ private TlsConfiguration tlsConfiguration;
private int httpMinorVersion = 1; // Assume HTTP/1.1
private long idleStartTimeNs;
private Handshake handshake;
@@ -142,28 +146,43 @@ public final class Connection implements Closeable {
throws IOException {
if (connected) throw new IllegalStateException("already connected");
- if (route.proxy.type() == Proxy.Type.DIRECT || route.proxy.type() == Proxy.Type.HTTP) {
- socket = route.address.socketFactory.createSocket();
- } else {
- socket = new Socket(route.proxy);
+ TlsFallbackStrategy tlsFallbackStrategy = null;
+ if (route.address.sslSocketFactory != null) {
+ tlsFallbackStrategy = TlsFallbackStrategy.create();
}
- socket.setSoTimeout(readTimeout);
- Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
+ while (!connected) {
+ if (route.proxy.type() == Proxy.Type.DIRECT || route.proxy.type() == Proxy.Type.HTTP) {
+ socket = route.address.socketFactory.createSocket();
+ } else {
+ socket = new Socket(route.proxy);
+ }
+
+ socket.setSoTimeout(readTimeout);
+ Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout);
- if (route.address.sslSocketFactory != null) {
- upgradeToTls(tunnelRequest);
- } else {
- httpConnection = new HttpConnection(pool, this, socket);
+ if (tlsFallbackStrategy != null) {
+ boolean success = upgradeToTls(tlsFallbackStrategy, tunnelRequest);
+ if (!success) {
+ continue;
+ }
+ } else {
+ httpConnection = new HttpConnection(pool, this, socket);
+ }
+ connected = true;
}
- connected = true;
}
/**
- * Create an {@code SSLSocket} and perform the TLS handshake and certificate
- * validation.
+ * Create an {@code SSLSocket} and perform the TLS handshake and certificate validation.
+ *
+ * Returns {@code true} if the connection was successful, {@code false} if the connection was
+ * unsuccessful but should be retried and throws an {@link IOException} if the connection failed
+ * in a non-retryable fashion.
*/
- private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException {
+ private boolean upgradeToTls(TlsFallbackStrategy tlsFallbackStrategy, TunnelRequest tunnelRequest)
+ throws IOException {
+
Platform platform = Platform.get();
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
@@ -171,56 +190,64 @@ public final class Connection implements Closeable {
makeTunnel(tunnelRequest);
}
- // Create the wrapper over connected socket.
- socket = route.address.sslSocketFactory
- .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
- SSLSocket sslSocket = (SSLSocket) socket;
- if (route.modernTls) {
- platform.enableTlsExtensions(sslSocket, route.address.uriHost);
- } else {
- platform.supportTlsIntolerantServer(sslSocket);
- }
-
- boolean useNpn = false;
- if (route.modernTls) {
- boolean http2 = route.address.protocols.contains(Protocol.HTTP_2);
- boolean spdy3 = route.address.protocols.contains(Protocol.SPDY_3);
- if (http2 && spdy3) {
- platform.setNpnProtocols(sslSocket, Protocol.HTTP2_SPDY3_AND_HTTP);
- useNpn = true;
- } else if (http2) {
- platform.setNpnProtocols(sslSocket, Protocol.HTTP2_AND_HTTP_11);
- useNpn = true;
- } else if (spdy3) {
- platform.setNpnProtocols(sslSocket, Protocol.SPDY3_AND_HTTP11);
- useNpn = true;
+ try {
+ // Create the wrapper over connected socket.
+ socket = route.address.sslSocketFactory
+ .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */);
+ SSLSocket sslSocket = (SSLSocket) socket;
+
+ TlsConfiguration tlsConfiguration =
+ tlsFallbackStrategy.configureSecureSocket(sslSocket, route.address.uriHost, platform);
+ boolean useNpn = tlsConfiguration.supportsNpn();
+ if (useNpn) {
+ boolean http2 = route.address.protocols.contains(Protocol.HTTP_2);
+ boolean spdy3 = route.address.protocols.contains(Protocol.SPDY_3);
+ if (http2 && spdy3) {
+ platform.setNpnProtocols(sslSocket, Protocol.HTTP2_SPDY3_AND_HTTP);
+ } else if (http2) {
+ platform.setNpnProtocols(sslSocket, Protocol.HTTP2_AND_HTTP_11);
+ } else if (spdy3) {
+ platform.setNpnProtocols(sslSocket, Protocol.SPDY3_AND_HTTP11);
+ }
}
- }
- // Force handshake. This can throw!
- sslSocket.startHandshake();
+ // Force handshake. This can throw!
+ sslSocket.startHandshake();
- // Verify that the socket's certificates are acceptable for the target host.
- if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
- throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
- }
+ // Verify that the socket's certificates are acceptable for the target host.
+ if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) {
+ throw new IOException("Hostname '" + route.address.uriHost + "' was not verified");
+ }
- handshake = Handshake.get(sslSocket.getSession());
+ handshake = Handshake.get(sslSocket.getSession());
- ByteString maybeProtocol;
- Protocol selectedProtocol = Protocol.HTTP_11;
- if (useNpn && (maybeProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
- selectedProtocol = Protocol.find(maybeProtocol); // Throws IOE on unknown.
- }
+ ByteString maybeProtocol;
+ Protocol selectedProtocol = Protocol.HTTP_11;
+ if (useNpn && (maybeProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) {
+ selectedProtocol = Protocol.find(maybeProtocol); // Throws IOE on unknown.
+ }
- if (selectedProtocol.spdyVariant) {
- sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
- spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
- .protocol(selectedProtocol).build();
- spdyConnection.sendConnectionHeader();
- } else {
- httpConnection = new HttpConnection(pool, this, socket);
+ if (selectedProtocol.spdyVariant) {
+ sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream.
+ spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, socket)
+ .protocol(selectedProtocol).build();
+ spdyConnection.sendConnectionHeader();
+ } else {
+ httpConnection = new HttpConnection(pool, this, socket);
+ }
+ this.tlsConfiguration = tlsConfiguration;
+ } catch (IOException e){
+ boolean retryConnect = tlsFallbackStrategy.connectionFailed(e);
+ if (retryConnect) {
+ closeQuietly(socket);
+ handshake = null;
+ socket = null;
+ return false;
+ }
+ throw e;
}
+
+ return true;
}
/** Returns true if {@link #connect} has been attempted on this connection. */
@@ -237,6 +264,10 @@ public final class Connection implements Closeable {
return route;
}
+ public TlsConfiguration getTlsConfiguration() {
+ return tlsConfiguration;
+ }
+
/**
* Returns the socket that this connection uses, or null if the connection
* is not currently connected.
diff --git a/okhttp/src/main/java/com/squareup/okhttp/Route.java b/okhttp/src/main/java/com/squareup/okhttp/Route.java
index a08a469..4f99075 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/Route.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/Route.java
@@ -28,8 +28,6 @@ import java.net.Proxy;
* <li><strong>IP address:</strong> whether connecting directly to an origin
* server or a proxy, opening a socket requires an IP address. The DNS
* server may return multiple IP addresses to attempt.
- * <li><strong>Modern TLS:</strong> whether to include advanced TLS options
- * when attempting a HTTPS connection.
* </ul>
* Each route is a specific selection of these options.
*/
@@ -37,17 +35,14 @@ public class Route {
final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
- final boolean modernTls;
- public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress,
- boolean modernTls) {
+ public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {
if (address == null) throw new NullPointerException("address == null");
if (proxy == null) throw new NullPointerException("proxy == null");
if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null");
this.address = address;
this.proxy = proxy;
this.inetSocketAddress = inetSocketAddress;
- this.modernTls = modernTls;
}
/** Returns the {@link Address} of this route. */
@@ -70,18 +65,12 @@ public class Route {
return inetSocketAddress;
}
- /** Returns true if this route uses modern TLS. */
- public boolean isModernTls() {
- return modernTls;
- }
-
@Override public boolean equals(Object obj) {
if (obj instanceof Route) {
Route other = (Route) obj;
return (address.equals(other.address)
&& proxy.equals(other.proxy)
- && inetSocketAddress.equals(other.inetSocketAddress)
- && modernTls == other.modernTls);
+ && inetSocketAddress.equals(other.inetSocketAddress));
}
return false;
}
@@ -91,7 +80,6 @@ public class Route {
result = 31 * result + address.hashCode();
result = 31 * result + proxy.hashCode();
result = 31 * result + inetSocketAddress.hashCode();
- result = result + (modernTls ? (31 * result) : 0);
return result;
}
}
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 231ce3b..28df601 100644
--- a/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/Platform.java
@@ -17,6 +17,7 @@
package com.squareup.okhttp.internal;
import com.squareup.okhttp.Protocol;
+
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
@@ -93,20 +94,7 @@ public class Platform {
return url.toURI(); // this isn't as good as the built-in toUriLenient
}
- /**
- * Attempt a TLS connection with useful extensions enabled. This mode
- * supports more features, but is less likely to be compatible with older
- * HTTPS servers.
- */
- public void enableTlsExtensions(SSLSocket socket, String uriHost) {
- }
-
- /**
- * Attempt a secure connection with basic functionality to maximize
- * compatibility. Currently this uses SSL 3.0.
- */
- public void supportTlsIntolerantServer(SSLSocket socket) {
- socket.setEnabledProtocols(new String[] {"SSLv3"});
+ public void configureSecureSocket(SSLSocket socket, String uriHost, boolean isFallback) {
}
/** Returns the negotiated protocol, or null if no protocol was negotiated. */
@@ -243,8 +231,10 @@ public class Platform {
}
}
- @Override public void enableTlsExtensions(SSLSocket socket, String uriHost) {
- super.enableTlsExtensions(socket, uriHost);
+ @Override public void configureSecureSocket(SSLSocket socket, String uriHost,
+ boolean isFallback) {
+
+ super.configureSecureSocket(socket, uriHost, isFallback);
if (!openSslSocketClass.isInstance(socket)) return;
try {
setUseSessionTickets.invoke(socket, true);
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java b/okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java
new file mode 100644
index 0000000..504844d
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/TlsConfiguration.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal;
+
+import java.util.Arrays;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * A configuration of desired secure socket protocols.
+ */
+public class TlsConfiguration {
+ static final String SSL_V3 = "SSLv3";
+
+ public static final TlsConfiguration USE_DEFAULT = new TlsConfiguration(null, true);
+
+ public static final TlsConfiguration SSL_V3_ONLY =
+ new TlsConfiguration(new String[] { SSL_V3 }, false /* supportsNpn */);
+
+ // The set of all protocols. Can be null. If non-null it must have at least one item in it, which
+ // must be supported. All others are considered optional.
+ private final String[] protocols;
+ private final boolean supportsNpn;
+
+ /**
+ * Creates a {@link TlsConfiguration} with the specified settings.
+ *
+ * <p>If {@code protocols} is {@code null} this means "use the default socket configuration".
+ *
+ * <p>If {@code protocols} in non-null it must contain at least one value. The ordering of the
+ * protocols is important: the first protocol specified <em>must</em> be supported by a socket
+ * for the {@link #isCompatible(javax.net.ssl.SSLSocket)} method to return {@code true}.
+ * The other protocols are considered optional. {@code protocols} must not contain null values.
+ */
+ private TlsConfiguration(String[] protocols, boolean supportsNpn) {
+ if (protocols != null && protocols.length < 1) {
+ throw new IllegalArgumentException("protocols must contain at least one protocol");
+ }
+
+ this.protocols = protocols;
+ this.supportsNpn = supportsNpn;
+ }
+
+ public boolean supportsNpn() {
+ return supportsNpn;
+ }
+
+ /**
+ * Returns {@code true} if the socket, as currently configured, supports this configuration.
+ */
+ public boolean isCompatible(SSLSocket socket) {
+ if (protocols == null) {
+ // No primary protocol means "use default".
+ return true;
+ }
+
+ // We use enabled protocols here, not supported, to avoid re-enabling a protocol that has
+ // been disabled. Just because something is supported does not make it desirable to use.
+ String[] enabledProtocols = socket.getEnabledProtocols();
+ return contains(enabledProtocols, protocols[0]);
+ }
+
+ public void configureProtocols(SSLSocket socket) {
+ if (protocols != null) {
+ // We use enabled protocols here, not supported, to avoid re-enabling a protocol that has
+ // been disabled. Just because something is supported does not make it desirable to use.
+ String[] enabledProtocols = socket.getEnabledProtocols();
+
+ // Create an array to hold the subset of protocols that are desired, and copy across the
+ // enabled protocols that intersect.
+ String[] desiredProtocols = new String[protocols.length];
+ int desiredIndex = 0;
+ for (int protocolsIndex = 0; protocolsIndex < protocols.length; protocolsIndex++) {
+ String candidateProtocol = protocols[protocolsIndex];
+ if (contains(enabledProtocols, candidateProtocol)) {
+ desiredProtocols[desiredIndex++] = candidateProtocol;
+ } else if (desiredIndex == 0) {
+ // This is checked by isCompatible.
+ throw new AssertionError("primaryProtocol " + candidateProtocol + " is not supported");
+ }
+ }
+
+ // Shrink the desiredProtocols array to the correct size.
+ if (desiredIndex < desiredProtocols.length) {
+ String[] desiredCopy = new String[desiredIndex];
+ System.arraycopy(desiredProtocols, 0, desiredCopy, 0, desiredIndex);
+ desiredProtocols = desiredCopy;
+ }
+
+ socket.setEnabledProtocols(desiredProtocols);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "TlsConfiguration{" +
+ "protocols=" + Arrays.toString(protocols) +
+ ", supportsNpn=" + supportsNpn +
+ '}';
+ }
+
+ private static <T> boolean contains(T[] array, T value) {
+ for (T arrayValue : array) {
+ if (value != null && value.equals(arrayValue)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java b/okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java
new file mode 100644
index 0000000..6ee5ed5
--- /dev/null
+++ b/okhttp/src/main/java/com/squareup/okhttp/internal/TlsFallbackStrategy.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.squareup.okhttp.internal;
+
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.util.Arrays;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLProtocolException;
+import javax.net.ssl.SSLSocket;
+
+/**
+ * Handles the socket protocol connection fallback strategy: When a secure socket connection fails
+ * due to a handshake / protocol problem the connection may be retried with different protocols.
+ * Instances are stateful and should be created and used for a single connection attempt.
+ */
+public class TlsFallbackStrategy {
+
+ private final TlsConfiguration[] configurations;
+ private int nextModeIndex;
+ private boolean isFallbackPossible;
+ private boolean isFallback;
+
+ /** Create a new {@link TlsFallbackStrategy}. */
+ public static TlsFallbackStrategy create() {
+ return new TlsFallbackStrategy(TlsConfiguration.USE_DEFAULT, TlsConfiguration.SSL_V3_ONLY);
+ }
+
+ /** Use {@link #create()} */
+ private TlsFallbackStrategy(TlsConfiguration... configurations) {
+ this.nextModeIndex = 0;
+ this.configurations = configurations;
+ }
+
+ /**
+ * Configure the supplied {@link SSLSocket} to connect to the specified host using an appropriate
+ * {@link TlsConfiguration}.
+ *
+ * @return the chosen {@link TlsConfiguration}
+ * @throws IOException if the socket does not support any of the tls modes available
+ */
+ public TlsConfiguration configureSecureSocket(SSLSocket sslSocket, String host, Platform platform)
+ throws IOException {
+
+ TlsConfiguration tlsConfiguration = null;
+ for (int i = nextModeIndex; i < configurations.length; i++) {
+ if (configurations[i].isCompatible(sslSocket)) {
+ tlsConfiguration = configurations[i];
+ nextModeIndex = i + 1;
+ break;
+ }
+ }
+
+ if (tlsConfiguration == null) {
+ // This may be the first time a connection has been attempted and the socket does not support
+ // any the required protocols, or it may be a retry (but this socket supports fewer
+ // protocols than was suggested by a prior socket).
+ throw new IOException(
+ "Unable to find acceptable protocols. isFallback=" + isFallback +
+ ", modes=" + Arrays.toString(configurations) +
+ ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
+ }
+
+ isFallbackPossible = isFallbackPossible(sslSocket);
+
+ tlsConfiguration.configureProtocols(sslSocket);
+ platform.configureSecureSocket(sslSocket, host, isFallback);
+ return tlsConfiguration;
+ }
+
+ /**
+ * Reports a failure to complete a connection. Determines the next {@link TlsConfiguration} to
+ * try, if any.
+ *
+ * @return {@code true} if the connection should be retried using
+ * {@link #configureSecureSocket(SSLSocket, String, Platform)} or {@code false} if not
+ */
+ public boolean connectionFailed(IOException e) {
+ // Any future attempt to connect using this strategy will be a fallback attempt.
+ isFallback = true;
+
+ if (e instanceof SSLHandshakeException) {
+ // If the problem was a CertificateException from the X509TrustManager,
+ // do not retry.
+ if (e.getCause() instanceof CertificateException) {
+ return false;
+ }
+ }
+
+ // TODO(nfuller): should we retry SSLProtocolExceptions at all? SSLProtocolExceptions can be
+ // caused by TLS_FALLBACK_SCSV failures, which means we retry those when we probably should not.
+ return ((e instanceof SSLHandshakeException || e instanceof SSLProtocolException))
+ && isFallbackPossible;
+ }
+
+ /**
+ * Returns {@code true} if any later {@link TlsConfiguration} in the fallback strategy looks
+ * possible based on the supplied {@link SSLSocket}. It assumes that a future socket will have the
+ * same capabilities as the supplied socket.
+ */
+ private boolean isFallbackPossible(SSLSocket socket) {
+ for (int i = nextModeIndex; i < configurations.length; i++) {
+ if (configurations[i].isCompatible(socket)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java b/okhttp/src/main/java/com/squareup/okhttp/internal/http/HttpEngine.java
index d796a6c..0013226 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.Connection;
import com.squareup.okhttp.Headers;
-import com.squareup.okhttp.HostResolver;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.OkResponseCache;
import com.squareup.okhttp.Request;
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 f1220a9..a5ffa77 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
@@ -33,23 +33,14 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
-import javax.net.ssl.SSLHandshakeException;
-import javax.net.ssl.SSLProtocolException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
/**
* Selects routes to connect to an origin server. Each connection requires a
- * choice of proxy server, IP address, and TLS mode. Connections may also be
- * recycled.
+ * choice of proxy server and IP address. Connections may also be recycled.
*/
public final class RouteSelector {
- /** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */
- private static final int TLS_MODE_MODERN = 1;
- /** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */
- private static final int TLS_MODE_COMPATIBLE = 0;
- /** No TLS mode. */
- private static final int TLS_MODE_NULL = -1;
private final Address address;
private final URI uri;
@@ -72,9 +63,6 @@ public final class RouteSelector {
private int nextSocketAddressIndex;
private int socketPort;
- /* State for negotiating the next TLS configuration */
- private int nextTlsMode = TLS_MODE_NULL;
-
/* State for negotiating failed routes */
private final List<Route> postponedRoutes;
@@ -96,7 +84,7 @@ public final class RouteSelector {
* least one route.
*/
public boolean hasNext() {
- return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
+ return hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
}
/**
@@ -112,23 +100,19 @@ public final class RouteSelector {
}
// Compute the next route to attempt.
- if (!hasNextTlsMode()) {
- if (!hasNextInetSocketAddress()) {
- if (!hasNextProxy()) {
- if (!hasNextPostponed()) {
- throw new NoSuchElementException();
- }
- return new Connection(pool, nextPostponed());
+ if (!hasNextInetSocketAddress()) {
+ if (!hasNextProxy()) {
+ if (!hasNextPostponed()) {
+ throw new NoSuchElementException();
}
- lastProxy = nextProxy();
- resetNextInetSocketAddress(lastProxy);
+ return new Connection(pool, nextPostponed());
}
- lastInetSocketAddress = nextInetSocketAddress();
- resetNextTlsMode();
+ lastProxy = nextProxy();
+ resetNextInetSocketAddress(lastProxy);
}
+ lastInetSocketAddress = nextInetSocketAddress();
- boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
- Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
+ Route route = new Route(address, lastProxy, lastInetSocketAddress);
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
// We will only recurse in order to skip previously failed routes. They will be
@@ -154,17 +138,6 @@ public final class RouteSelector {
}
routeDatabase.failed(failedRoute);
-
- // If the previously returned route's problem was not related to TLS, and
- // the next route only changes the TLS mode, we shouldn't even attempt it.
- // This suppresses it in both this selector and also in the route database.
- if (hasNextTlsMode()
- && !(failure instanceof SSLHandshakeException)
- && !(failure instanceof SSLProtocolException)) {
- boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
- Route routeToSuppress = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
- routeDatabase.failed(routeToSuppress);
- }
}
/** Resets {@link #nextProxy} to the first option. */
@@ -250,29 +223,6 @@ public final class RouteSelector {
return result;
}
- /** Resets {@link #nextTlsMode} to the first option. */
- private void resetNextTlsMode() {
- nextTlsMode = (address.getSslSocketFactory() != null) ? TLS_MODE_MODERN : TLS_MODE_COMPATIBLE;
- }
-
- /** Returns true if there's another TLS mode to try. */
- private boolean hasNextTlsMode() {
- return nextTlsMode != TLS_MODE_NULL;
- }
-
- /** Returns the next TLS mode to try. */
- private int nextTlsMode() {
- if (nextTlsMode == TLS_MODE_MODERN) {
- nextTlsMode = TLS_MODE_COMPATIBLE;
- return TLS_MODE_MODERN;
- } else if (nextTlsMode == TLS_MODE_COMPATIBLE) {
- nextTlsMode = TLS_MODE_NULL; // So that hasNextTlsMode() returns false.
- return TLS_MODE_COMPATIBLE;
- } else {
- throw new AssertionError();
- }
- }
-
/** Returns true if there is another postponed route to try. */
private boolean hasNextPostponed() {
return !postponedRoutes.isEmpty();