diff options
| author | Neil Fuller <nfuller@google.com> | 2014-11-21 16:47:45 +0000 |
|---|---|---|
| committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2014-11-21 16:47:46 +0000 |
| commit | 80a1db2828f1a1ae290a6cc8105616666fa97e73 (patch) | |
| tree | 7de099975388df74f08974549ce1ec40670566c4 | |
| parent | fcc9fffc119d3ecb6482c04875eec2e6e0378c88 (diff) | |
| parent | bcce0a3d26d66d33beb742ae2adddb3b7db5ad08 (diff) | |
| download | android_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"
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(); |
