blob: 5e0f7d868a953a0ad4fa5f891f5d89afac1326e8 [file] [log] [blame]
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.Util;
import java.util.Arrays;
import java.util.List;
import javax.net.ssl.SSLSocket;
/**
* Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
* https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
* connection.
*/
public final class ConnectionSpec {
// This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5.
// All of these suites are available on Android 5.0; earlier releases support a subset of
// these suites. https://github.com/square/okhttp/issues/330
private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
// Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
// continue to include them until better suites are commonly available. For example, none
// of the better cipher suites listed above shipped with Android 4.4 or Java 7.
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
};
/** A modern TLS connection with extensions like SNI and ALPN available. */
public static final ConnectionSpec MODERN_TLS = new Builder(true)
.cipherSuites(APPROVED_CIPHER_SUITES)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
.supportsTlsExtensions(true)
.build();
/** A backwards-compatible fallback connection for interop with obsolete servers. */
public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_0)
.supportsTlsExtensions(true)
.build();
/** Unencrypted, unauthenticated connections for {@code http:} URLs. */
public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
final boolean tls;
/**
* Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use
* default set".
*/
private final String[] cipherSuites;
/** Used if tls == true. The TLS protocol versions to use. */
private final String[] tlsVersions;
final boolean supportsTlsExtensions;
private ConnectionSpec(Builder builder) {
this.tls = builder.tls;
this.cipherSuites = builder.cipherSuites;
this.tlsVersions = builder.tlsVersions;
this.supportsTlsExtensions = builder.supportsTlsExtensions;
}
public boolean isTls() {
return tls;
}
/**
* 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.
*/
public List<CipherSuite> cipherSuites() {
if (cipherSuites == null) {
return null;
}
CipherSuite[] result = new CipherSuite[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
result[i] = CipherSuite.forJavaName(cipherSuites[i]);
}
return Util.immutableList(result);
}
public List<TlsVersion> tlsVersions() {
TlsVersion[] result = new TlsVersion[tlsVersions.length];
for (int i = 0; i < tlsVersions.length; i++) {
result[i] = TlsVersion.forJavaName(tlsVersions[i]);
}
return Util.immutableList(result);
}
public boolean supportsTlsExtensions() {
return supportsTlsExtensions;
}
/** Applies this spec to {@code sslSocket}. */
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);
}
}
/**
* 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);
}
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)
.build();
}
/**
* Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec.
* 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
* match the socket's enabled cipher suites. If there are no required cipher suites the socket
* must have at least one cipher suite enabled.
*
* <p>For protocols, at least one of the {@link #tlsVersions() required protocols} must match the
* socket's enabled protocols.
*/
public boolean isCompatible(SSLSocket socket) {
if (!tls) {
return false;
}
String[] enabledProtocols = socket.getEnabledProtocols();
boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols);
if (!requiredProtocolsEnabled) {
return false;
}
boolean requiredCiphersEnabled;
if (cipherSuites == null) {
requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0;
} else {
String[] enabledCipherSuites = socket.getEnabledCipherSuites();
requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites);
}
return requiredCiphersEnabled;
}
/**
* An N*M intersection that terminates if any intersection is found. The sizes of both
* arguments are assumed to be so small, and the likelihood of an intersection so great, that it
* is not worth the CPU cost of sorting or the memory cost of hashing.
*/
private static boolean nonEmptyIntersection(String[] a, String[] b) {
if (a == null || b == null || a.length == 0 || b.length == 0) {
return false;
}
for (String toFind : a) {
if (contains(b, toFind)) {
return true;
}
}
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;
ConnectionSpec that = (ConnectionSpec) other;
if (this.tls != that.tls) return false;
if (tls) {
if (!Arrays.equals(this.cipherSuites, that.cipherSuites)) return false;
if (!Arrays.equals(this.tlsVersions, that.tlsVersions)) return false;
if (this.supportsTlsExtensions != that.supportsTlsExtensions) return false;
}
return true;
}
@Override public int hashCode() {
int result = 17;
if (tls) {
result = 31 * result + Arrays.hashCode(cipherSuites);
result = 31 * result + Arrays.hashCode(tlsVersions);
result = 31 * result + (supportsTlsExtensions ? 0 : 1);
}
return result;
}
@Override public String toString() {
if (tls) {
List<CipherSuite> cipherSuites = cipherSuites();
String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString();
return "ConnectionSpec(cipherSuites=" + cipherSuitesString
+ ", tlsVersions=" + tlsVersions()
+ ", supportsTlsExtensions=" + supportsTlsExtensions
+ ")";
} else {
return "ConnectionSpec()";
}
}
public static final class Builder {
private boolean tls;
private String[] cipherSuites;
private String[] tlsVersions;
private boolean supportsTlsExtensions;
Builder(boolean tls) {
this.tls = tls;
}
public Builder(ConnectionSpec connectionSpec) {
this.tls = connectionSpec.tls;
this.cipherSuites = connectionSpec.cipherSuites;
this.tlsVersions = connectionSpec.tlsVersions;
this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
}
public Builder cipherSuites(CipherSuite... cipherSuites) {
if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
// Convert enums to the string names Java wants. This makes a defensive copy!
String[] strings = new String[cipherSuites.length];
for (int i = 0; i < cipherSuites.length; i++) {
strings[i] = cipherSuites[i].javaName;
}
this.cipherSuites = strings;
return this;
}
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();
}
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;
}
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();
}
return this;
}
public Builder supportsTlsExtensions(boolean supportsTlsExtensions) {
if (!tls) throw new IllegalStateException("no TLS extensions for cleartext connections");
this.supportsTlsExtensions = supportsTlsExtensions;
return this;
}
public ConnectionSpec build() {
return new ConnectionSpec(this);
}
}
}