1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-17 08:42:25 +03:00

Make ConnectionSpec more uniform.

Closes https://github.com/square/okhttp/issues/1831
This commit is contained in:
jwilson
2015-11-02 20:18:28 -05:00
parent f20329132f
commit 784fc03fd4
4 changed files with 210 additions and 127 deletions

View File

@@ -15,29 +15,49 @@
*/
package com.squareup.okhttp;
import org.junit.Test;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public final class ConnectionSpecTest {
@Test public void noTlsVersions() throws Exception {
try {
new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(new TlsVersion[0])
.build();
fail();
} catch (IllegalArgumentException expected) {
assertEquals("At least one TLS version is required", expected.getMessage());
}
}
@Test
public void cleartextBuilder() throws Exception {
@Test public void noCipherSuites() throws Exception {
try {
new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(new CipherSuite[0])
.build();
fail();
} catch (IllegalArgumentException expected) {
assertEquals("At least one cipher suite is required", expected.getMessage());
}
}
@Test public void cleartextBuilder() throws Exception {
ConnectionSpec cleartextSpec = new ConnectionSpec.Builder(false).build();
assertFalse(cleartextSpec.isTls());
}
@Test
public void tlsBuilder_explicitCiphers() throws Exception {
@Test public void tlsBuilder_explicitCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -48,8 +68,7 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.supportsTlsExtensions());
}
@Test
public void tlsBuilder_defaultCiphers() throws Exception {
@Test public void tlsBuilder_defaultCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(true)
@@ -59,8 +78,7 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.supportsTlsExtensions());
}
@Test
public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
@Test public void tls_defaultCiphers_noFallbackIndicator() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(false)
@@ -79,17 +97,16 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, false /* isFallback */);
assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet =
createSet(
set(
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
assertEquals(expectedCipherSet, expectedCipherSet);
}
@Test
public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
@Test public void tls_defaultCiphers_withFallbackIndicator() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.tlsVersions(TlsVersion.TLS_1_2)
.supportsTlsExtensions(false)
@@ -108,10 +125,10 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, true /* isFallback */);
assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet =
createSet(
set(
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName);
if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
@@ -120,8 +137,7 @@ public final class ConnectionSpecTest {
assertEquals(expectedCipherSet, expectedCipherSet);
}
@Test
public void tls_explicitCiphers() throws Exception {
@Test public void tls_explicitCiphers() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -141,17 +157,16 @@ public final class ConnectionSpecTest {
assertTrue(tlsSpec.isCompatible(socket));
tlsSpec.apply(socket, true /* isFallback */);
assertEquals(createSet(TlsVersion.TLS_1_2.javaName), createSet(socket.getEnabledProtocols()));
assertEquals(set(TlsVersion.TLS_1_2.javaName), set(socket.getEnabledProtocols()));
Set<String> expectedCipherSet = createSet(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
Set<String> expectedCipherSet = set(CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName);
if (Arrays.asList(socket.getSupportedCipherSuites()).contains("TLS_FALLBACK_SCSV")) {
expectedCipherSet.add("TLS_FALLBACK_SCSV");
}
assertEquals(expectedCipherSet, expectedCipherSet);
}
@Test
public void tls_stringCiphersAndVersions() throws Exception {
@Test public void tls_stringCiphersAndVersions() throws Exception {
// Supporting arbitrary input strings allows users to enable suites and versions that are not
// yet known to the library, but are supported by the platform.
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
@@ -160,7 +175,7 @@ public final class ConnectionSpecTest {
.build();
}
public void tls_missingRequiredCipher() throws Exception {
@Test public void tls_missingRequiredCipher() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -185,8 +200,43 @@ public final class ConnectionSpecTest {
assertFalse(tlsSpec.isCompatible(socket));
}
@Test
public void tls_missingTlsVersion() throws Exception {
@Test public void allEnabledCipherSuites() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledCipherSuites()
.build();
assertNull(tlsSpec.cipherSuites());
SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
sslSocket.setEnabledCipherSuites(new String[] {
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName,
});
tlsSpec.apply(sslSocket, false);
assertEquals(Arrays.asList(
CipherSuite.TLS_RSA_WITH_RC4_128_SHA.javaName,
CipherSuite.TLS_RSA_WITH_RC4_128_MD5.javaName),
Arrays.asList(sslSocket.getEnabledCipherSuites()));
}
@Test public void allEnabledTlsVersions() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledTlsVersions()
.build();
assertNull(tlsSpec.tlsVersions());
SSLSocket sslSocket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
sslSocket.setEnabledProtocols(new String[] {
TlsVersion.SSL_3_0.javaName(),
TlsVersion.TLS_1_1.javaName()
});
tlsSpec.apply(sslSocket, false);
assertEquals(Arrays.asList(TlsVersion.SSL_3_0.javaName(), TlsVersion.TLS_1_1.javaName()),
Arrays.asList(sslSocket.getEnabledProtocols()));
}
@Test public void tls_missingTlsVersion() throws Exception {
ConnectionSpec tlsSpec = new ConnectionSpec.Builder(true)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.tlsVersions(TlsVersion.TLS_1_2)
@@ -206,7 +256,48 @@ public final class ConnectionSpecTest {
assertFalse(tlsSpec.isCompatible(socket));
}
private static Set<String> createSet(String... values) {
return new LinkedHashSet<String>(Arrays.asList(values));
@Test public void equalsAndHashCode() throws Exception {
ConnectionSpec allCipherSuites = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledCipherSuites()
.build();
ConnectionSpec allTlsVersions = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledTlsVersions()
.build();
Set<Object> set = new CopyOnWriteArraySet<>();
assertTrue(set.add(ConnectionSpec.MODERN_TLS));
assertTrue(set.add(ConnectionSpec.COMPATIBLE_TLS));
assertTrue(set.add(ConnectionSpec.CLEARTEXT));
assertTrue(set.add(allTlsVersions));
assertTrue(set.add(allCipherSuites));
assertTrue(set.remove(ConnectionSpec.MODERN_TLS));
assertTrue(set.remove(ConnectionSpec.COMPATIBLE_TLS));
assertTrue(set.remove(ConnectionSpec.CLEARTEXT));
assertTrue(set.remove(allTlsVersions));
assertTrue(set.remove(allCipherSuites));
assertTrue(set.isEmpty());
}
@Test public void allEnabledToString() throws Exception {
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.allEnabledTlsVersions()
.allEnabledCipherSuites()
.build();
assertEquals("ConnectionSpec(cipherSuites=[all enabled], tlsVersions=[all enabled], "
+ "supportsTlsExtensions=true)", connectionSpec.toString());
}
@Test public void simpleToString() throws Exception {
ConnectionSpec connectionSpec = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2)
.cipherSuites(CipherSuite.TLS_RSA_WITH_RC4_128_MD5)
.build();
assertEquals("ConnectionSpec(cipherSuites=[TLS_RSA_WITH_RC4_128_MD5], tlsVersions=[TLS_1_2], "
+ "supportsTlsExtensions=true)", connectionSpec.toString());
}
private static <T> Set<T> set(T... values) {
return new LinkedHashSet<>(Arrays.asList(values));
}
}

View File

@@ -20,10 +20,20 @@ import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLSocket;
import static com.squareup.okhttp.internal.Util.concat;
import static com.squareup.okhttp.internal.Util.contains;
/**
* Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
* https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
* connection.
*
* <p>The TLS versions configured in a connection spec are only be used if they are also enabled in
* the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used
* even if it is present on the connection spec. The same policy also applies to cipher suites.
*
* <p>Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to
* defer all feature selection to the underlying SSL socket.
*/
public final class ConnectionSpec {
@@ -67,19 +77,11 @@ public final class ConnectionSpec {
/** Unencrypted, unauthenticated connections for {@code http:} URLs. */
public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
final boolean tls;
/**
* Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use
* default set".
*/
private final boolean tls;
private final boolean supportsTlsExtensions;
private final String[] cipherSuites;
/** Used if tls == true. The TLS protocol versions to use. */
private final String[] tlsVersions;
final boolean supportsTlsExtensions;
private ConnectionSpec(Builder builder) {
this.tls = builder.tls;
this.cipherSuites = builder.cipherSuites;
@@ -92,13 +94,12 @@ public final class ConnectionSpec {
}
/**
* Returns the cipher suites to use for a connection. This method can return {@code null} if the
* cipher suites enabled by default should be used.
* Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL
* socket's enabled cipher suites should be used.
*/
public List<CipherSuite> cipherSuites() {
if (cipherSuites == null) {
return null;
}
if (cipherSuites == null) return null;
CipherSuite[] result = new CipherSuite[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
result[i] = CipherSuite.forJavaName(cipherSuites[i]);
@@ -106,7 +107,13 @@ public final class ConnectionSpec {
return Util.immutableList(result);
}
/**
* Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of
* the SSL socket's enabled TLS versions should be used.
*/
public List<TlsVersion> tlsVersions() {
if (tlsVersions == null) return null;
TlsVersion[] result = new TlsVersion[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
result[i] = TlsVersion.forJavaName(tlsVersions[i]);
@@ -122,57 +129,40 @@ public final class ConnectionSpec {
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
sslSocket.setEnabledProtocols(specToApply.tlsVersions);
String[] cipherSuitesToEnable = specToApply.cipherSuites;
// null means "use default set".
if (cipherSuitesToEnable != null) {
sslSocket.setEnabledCipherSuites(cipherSuitesToEnable);
if (specToApply.tlsVersions != null) {
sslSocket.setEnabledProtocols(specToApply.tlsVersions);
}
if (specToApply.cipherSuites != null) {
sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
/**
* Returns a copy of this that omits cipher suites and TLS versions not enabled by
* {@code sslSocket}.
* Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
* sslSocket}.
*/
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
String[] cipherSuitesToEnable = null;
if (cipherSuites != null) {
String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites();
cipherSuitesToEnable =
Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom);
String[] cipherSuitesIntersection = cipherSuites != null
? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
: sslSocket.getEnabledCipherSuites();
String[] tlsVersionsIntersection = tlsVersions != null
? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
: sslSocket.getEnabledProtocols();
// In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
// the SCSV cipher is added to signal that a protocol fallback has taken place.
if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) {
cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
}
if (isFallback) {
// In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
// the SCSV cipher is added to signal that a protocol fallback has taken place.
final String fallbackScsv = "TLS_FALLBACK_SCSV";
boolean socketSupportsFallbackScsv =
Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv);
if (socketSupportsFallbackScsv) {
// Add the SCSV cipher to the set of enabled cipher suites iff it is supported.
String[] oldEnabledCipherSuites = cipherSuitesToEnable != null
? cipherSuitesToEnable
: sslSocket.getEnabledCipherSuites();
String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1];
System.arraycopy(oldEnabledCipherSuites, 0,
newEnabledCipherSuites, 0, oldEnabledCipherSuites.length);
newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
cipherSuitesToEnable = newEnabledCipherSuites;
}
}
String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols();
String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom);
return new Builder(this)
.cipherSuites(cipherSuitesToEnable)
.tlsVersions(protocolsToEnable)
.cipherSuites(cipherSuitesIntersection)
.tlsVersions(tlsVersionsIntersection)
.build();
}
/**
* Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec.
* Returns {@code true} if the socket, as currently configured, supports this connection spec.
* In order for a socket to be compatible the enabled cipher suites and protocols must intersect.
*
* <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
@@ -187,20 +177,17 @@ public final class ConnectionSpec {
return false;
}
String[] enabledProtocols = socket.getEnabledProtocols();
boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols);
if (!requiredProtocolsEnabled) {
if (tlsVersions != null
&& !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
return false;
}
boolean requiredCiphersEnabled;
if (cipherSuites == null) {
requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0;
} else {
String[] enabledCipherSuites = socket.getEnabledCipherSuites();
requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites);
if (cipherSuites != null
&& !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
return false;
}
return requiredCiphersEnabled;
return true;
}
/**
@@ -220,15 +207,6 @@ public final class ConnectionSpec {
return false;
}
private static <T> boolean contains(T[] array, T value) {
for (T arrayValue : array) {
if (Util.equal(value, arrayValue)) {
return true;
}
}
return false;
}
@Override public boolean equals(Object other) {
if (!(other instanceof ConnectionSpec)) return false;
if (other == this) return true;
@@ -256,16 +234,17 @@ public final class ConnectionSpec {
}
@Override public String toString() {
if (tls) {
List<CipherSuite> cipherSuites = cipherSuites();
String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString();
return "ConnectionSpec(cipherSuites=" + cipherSuitesString
+ ", tlsVersions=" + tlsVersions()
+ ", supportsTlsExtensions=" + supportsTlsExtensions
+ ")";
} else {
if (!tls) {
return "ConnectionSpec()";
}
String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]";
String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]";
return "ConnectionSpec("
+ "cipherSuites=" + cipherSuitesString
+ ", tlsVersions=" + tlsVersionsString
+ ", supportsTlsExtensions=" + supportsTlsExtensions
+ ")";
}
public static final class Builder {
@@ -285,56 +264,58 @@ public final class ConnectionSpec {
this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
}
public Builder allEnabledCipherSuites() {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
this.cipherSuites = null;
return this;
}
public Builder cipherSuites(CipherSuite... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
// Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
strings[i] = cipherSuites[i].javaName;
}
this.cipherSuites = strings;
return this;
return cipherSuites(strings);
}
public Builder cipherSuites(String... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
if (cipherSuites == null) {
this.cipherSuites = null;
} else {
// This makes a defensive copy!
this.cipherSuites = cipherSuites.clone();
if (cipherSuites.length == 0) {
throw new IllegalArgumentException("At least one cipher suite is required");
}
this.cipherSuites = cipherSuites.clone(); // Defensive copy.
return this;
}
public Builder allEnabledTlsVersions() {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
this.tlsVersions = null;
return this;
}
public Builder tlsVersions(TlsVersion... tlsVersions) {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
if (tlsVersions.length == 0) {
throw new IllegalArgumentException("At least one TlsVersion is required");
}
// Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
strings[i] = tlsVersions[i].javaName;
}
this.tlsVersions = strings;
return this;
return tlsVersions(strings);
}
public Builder tlsVersions(String... tlsVersions) {
if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
if (tlsVersions == null) {
this.tlsVersions = null;
} else {
// This makes a defensive copy!
this.tlsVersions = tlsVersions.clone();
if (tlsVersions.length == 0) {
throw new IllegalArgumentException("At least one TLS version is required");
}
this.tlsVersions = tlsVersions.clone(); // Defensive copy.
return this;
}

View File

@@ -30,7 +30,7 @@ public enum TlsVersion {
final String javaName;
private TlsVersion(String javaName) {
TlsVersion(String javaName) {
this.javaName = javaName;
}

View File

@@ -288,4 +288,15 @@ public final class Util {
return e.getCause() != null && e.getMessage() != null
&& e.getMessage().contains("getsockname failed");
}
public static boolean contains(String[] array, String value) {
return Arrays.asList(array).contains(value);
}
public static String[] concat(String[] array, String value) {
String[] result = new String[array.length + 1];
System.arraycopy(array, 0, result, 0, array.length);
result[result.length - 1] = value;
return result;
}
}