Home | History | Annotate | Download | only in okhttp
      1 /*
      2  * Copyright (C) 2014 Square, Inc.
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.squareup.okhttp;
     17 
     18 import com.squareup.okhttp.internal.Util;
     19 import java.util.Arrays;
     20 import java.util.List;
     21 import javax.net.ssl.SSLSocket;
     22 
     23 import static com.squareup.okhttp.internal.Util.concat;
     24 import static com.squareup.okhttp.internal.Util.contains;
     25 
     26 /**
     27  * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
     28  * https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
     29  * connection.
     30  *
     31  * <p>The TLS versions configured in a connection spec are only be used if they are also enabled in
     32  * the SSL socket. For example, if an SSL socket does not have TLS 1.2 enabled, it will not be used
     33  * even if it is present on the connection spec. The same policy also applies to cipher suites.
     34  *
     35  * <p>Use {@link Builder#allEnabledTlsVersions()} and {@link Builder#allEnabledCipherSuites} to
     36  * defer all feature selection to the underlying SSL socket.
     37  */
     38 public final class ConnectionSpec {
     39 
     40   // This is a subset of the cipher suites supported in Chrome 46, current as of 2015-11-05.
     41   // All of these suites are available on Android 5.0; earlier releases support a subset of
     42   // these suites. https://github.com/square/okhttp/issues/330
     43   private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
     44       CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
     45       CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
     46       CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
     47 
     48       // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
     49       // continue to include them until better suites are commonly available. For example, none
     50       // of the better cipher suites listed above shipped with Android 4.4 or Java 7.
     51       CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
     52       CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
     53       CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
     54       CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
     55       CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
     56       CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
     57       CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
     58       CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
     59       CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
     60       CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
     61   };
     62 
     63   /** A modern TLS connection with extensions like SNI and ALPN available. */
     64   public static final ConnectionSpec MODERN_TLS = new Builder(true)
     65       .cipherSuites(APPROVED_CIPHER_SUITES)
     66       .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
     67       .supportsTlsExtensions(true)
     68       .build();
     69 
     70   /** A backwards-compatible fallback connection for interop with obsolete servers. */
     71   public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
     72       .tlsVersions(TlsVersion.TLS_1_0)
     73       .supportsTlsExtensions(true)
     74       .build();
     75 
     76   /** Unencrypted, unauthenticated connections for {@code http:} URLs. */
     77   public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
     78 
     79   private final boolean tls;
     80   private final boolean supportsTlsExtensions;
     81   private final String[] cipherSuites;
     82   private final String[] tlsVersions;
     83 
     84   private ConnectionSpec(Builder builder) {
     85     this.tls = builder.tls;
     86     this.cipherSuites = builder.cipherSuites;
     87     this.tlsVersions = builder.tlsVersions;
     88     this.supportsTlsExtensions = builder.supportsTlsExtensions;
     89   }
     90 
     91   public boolean isTls() {
     92     return tls;
     93   }
     94 
     95   /**
     96    * Returns the cipher suites to use for a connection. Returns {@code null} if all of the SSL
     97    * socket's enabled cipher suites should be used.
     98    */
     99   public List<CipherSuite> cipherSuites() {
    100     if (cipherSuites == null) return null;
    101 
    102     CipherSuite[] result = new CipherSuite[cipherSuites.length];
    103     for (int i = 0; i < cipherSuites.length; i++) {
    104       result[i] = CipherSuite.forJavaName(cipherSuites[i]);
    105     }
    106     return Util.immutableList(result);
    107   }
    108 
    109   /**
    110    * Returns the TLS versions to use when negotiating a connection. Returns {@code null} if all of
    111    * the SSL socket's enabled TLS versions should be used.
    112    */
    113   public List<TlsVersion> tlsVersions() {
    114     if (tlsVersions == null) return null;
    115 
    116     TlsVersion[] result = new TlsVersion[tlsVersions.length];
    117     for (int i = 0; i < tlsVersions.length; i++) {
    118       result[i] = TlsVersion.forJavaName(tlsVersions[i]);
    119     }
    120     return Util.immutableList(result);
    121   }
    122 
    123   public boolean supportsTlsExtensions() {
    124     return supportsTlsExtensions;
    125   }
    126 
    127   /** Applies this spec to {@code sslSocket}. */
    128   void apply(SSLSocket sslSocket, boolean isFallback) {
    129     ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
    130 
    131     if (specToApply.tlsVersions != null) {
    132       sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    133     }
    134     if (specToApply.cipherSuites != null) {
    135       sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    136     }
    137   }
    138 
    139   /**
    140    * Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
    141    * sslSocket}.
    142    */
    143   private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
    144     String[] cipherSuitesIntersection = cipherSuites != null
    145         ? Util.intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
    146         : sslSocket.getEnabledCipherSuites();
    147     String[] tlsVersionsIntersection = tlsVersions != null
    148         ? Util.intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
    149         : sslSocket.getEnabledProtocols();
    150 
    151     // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
    152     // the SCSV cipher is added to signal that a protocol fallback has taken place.
    153     if (isFallback && contains(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV")) {
    154       cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
    155     }
    156 
    157     return new Builder(this)
    158         .cipherSuites(cipherSuitesIntersection)
    159         .tlsVersions(tlsVersionsIntersection)
    160         .build();
    161   }
    162 
    163   /**
    164    * Returns {@code true} if the socket, as currently configured, supports this connection spec.
    165    * In order for a socket to be compatible the enabled cipher suites and protocols must intersect.
    166    *
    167    * <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
    168    * match the socket's enabled cipher suites. If there are no required cipher suites the socket
    169    * must have at least one cipher suite enabled.
    170    *
    171    * <p>For protocols, at least one of the {@link #tlsVersions() required protocols} must match the
    172    * socket's enabled protocols.
    173    */
    174   public boolean isCompatible(SSLSocket socket) {
    175     if (!tls) {
    176       return false;
    177     }
    178 
    179     if (tlsVersions != null
    180         && !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
    181       return false;
    182     }
    183 
    184     if (cipherSuites != null
    185         && !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
    186       return false;
    187     }
    188 
    189     return true;
    190   }
    191 
    192   /**
    193    * An N*M intersection that terminates if any intersection is found. The sizes of both
    194    * arguments are assumed to be so small, and the likelihood of an intersection so great, that it
    195    * is not worth the CPU cost of sorting or the memory cost of hashing.
    196    */
    197   private static boolean nonEmptyIntersection(String[] a, String[] b) {
    198     if (a == null || b == null || a.length == 0 || b.length == 0) {
    199       return false;
    200     }
    201     for (String toFind : a) {
    202       if (contains(b, toFind)) {
    203         return true;
    204       }
    205     }
    206     return false;
    207   }
    208 
    209   @Override public boolean equals(Object other) {
    210     if (!(other instanceof ConnectionSpec)) return false;
    211     if (other == this) return true;
    212 
    213     ConnectionSpec that = (ConnectionSpec) other;
    214     if (this.tls != that.tls) return false;
    215 
    216     if (tls) {
    217       if (!Arrays.equals(this.cipherSuites, that.cipherSuites)) return false;
    218       if (!Arrays.equals(this.tlsVersions, that.tlsVersions)) return false;
    219       if (this.supportsTlsExtensions != that.supportsTlsExtensions) return false;
    220     }
    221 
    222     return true;
    223   }
    224 
    225   @Override public int hashCode() {
    226     int result = 17;
    227     if (tls) {
    228       result = 31 * result + Arrays.hashCode(cipherSuites);
    229       result = 31 * result + Arrays.hashCode(tlsVersions);
    230       result = 31 * result + (supportsTlsExtensions ? 0 : 1);
    231     }
    232     return result;
    233   }
    234 
    235   @Override public String toString() {
    236     if (!tls) {
    237       return "ConnectionSpec()";
    238     }
    239 
    240     String cipherSuitesString = cipherSuites != null ? cipherSuites().toString() : "[all enabled]";
    241     String tlsVersionsString = tlsVersions != null ? tlsVersions().toString() : "[all enabled]";
    242     return "ConnectionSpec("
    243         + "cipherSuites=" + cipherSuitesString
    244         + ", tlsVersions=" + tlsVersionsString
    245         + ", supportsTlsExtensions=" + supportsTlsExtensions
    246         + ")";
    247   }
    248 
    249   public static final class Builder {
    250     private boolean tls;
    251     private String[] cipherSuites;
    252     private String[] tlsVersions;
    253     private boolean supportsTlsExtensions;
    254 
    255     Builder(boolean tls) {
    256       this.tls = tls;
    257     }
    258 
    259     public Builder(ConnectionSpec connectionSpec) {
    260       this.tls = connectionSpec.tls;
    261       this.cipherSuites = connectionSpec.cipherSuites;
    262       this.tlsVersions = connectionSpec.tlsVersions;
    263       this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
    264     }
    265 
    266     public Builder allEnabledCipherSuites() {
    267       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
    268       this.cipherSuites = null;
    269       return this;
    270     }
    271 
    272     public Builder cipherSuites(CipherSuite... cipherSuites) {
    273       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
    274 
    275       String[] strings = new String[cipherSuites.length];
    276       for (int i = 0; i < cipherSuites.length; i++) {
    277         strings[i] = cipherSuites[i].javaName;
    278       }
    279       return cipherSuites(strings);
    280     }
    281 
    282     public Builder cipherSuites(String... cipherSuites) {
    283       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
    284 
    285       if (cipherSuites.length == 0) {
    286         throw new IllegalArgumentException("At least one cipher suite is required");
    287       }
    288 
    289       this.cipherSuites = cipherSuites.clone(); // Defensive copy.
    290       return this;
    291     }
    292 
    293     public Builder allEnabledTlsVersions() {
    294       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
    295       this.tlsVersions = null;
    296       return this;
    297     }
    298 
    299     public Builder tlsVersions(TlsVersion... tlsVersions) {
    300       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
    301 
    302       String[] strings = new String[tlsVersions.length];
    303       for (int i = 0; i < tlsVersions.length; i++) {
    304         strings[i] = tlsVersions[i].javaName;
    305       }
    306 
    307       return tlsVersions(strings);
    308     }
    309 
    310     public Builder tlsVersions(String... tlsVersions) {
    311       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
    312 
    313       if (tlsVersions.length == 0) {
    314         throw new IllegalArgumentException("At least one TLS version is required");
    315       }
    316 
    317       this.tlsVersions = tlsVersions.clone(); // Defensive copy.
    318       return this;
    319     }
    320 
    321     public Builder supportsTlsExtensions(boolean supportsTlsExtensions) {
    322       if (!tls) throw new IllegalStateException("no TLS extensions for cleartext connections");
    323       this.supportsTlsExtensions = supportsTlsExtensions;
    324       return this;
    325     }
    326 
    327     public ConnectionSpec build() {
    328       return new ConnectionSpec(this);
    329     }
    330   }
    331 }
    332