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 /**
     24  * Specifies configuration for the socket connection that HTTP traffic travels through. For {@code
     25  * https:} URLs, this includes the TLS version and cipher suites to use when negotiating a secure
     26  * connection.
     27  */
     28 public final class ConnectionSpec {
     29 
     30   // This is a subset of the cipher suites supported in Chrome 37, current as of 2014-10-5.
     31   // All of these suites are available on Android 5.0; earlier releases support a subset of
     32   // these suites. https://github.com/square/okhttp/issues/330
     33   private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
     34       CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
     35       CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
     36       CipherSuite.TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
     37 
     38       // Note that the following cipher suites are all on HTTP/2's bad cipher suites list. We'll
     39       // continue to include them until better suites are commonly available. For example, none
     40       // of the better cipher suites listed above shipped with Android 4.4 or Java 7.
     41       CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
     42       CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
     43       CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
     44       CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
     45       CipherSuite.TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
     46       CipherSuite.TLS_DHE_DSS_WITH_AES_128_CBC_SHA,
     47       CipherSuite.TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
     48       CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
     49       CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
     50       CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
     51       CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
     52   };
     53 
     54   /** A modern TLS connection with extensions like SNI and ALPN available. */
     55   public static final ConnectionSpec MODERN_TLS = new Builder(true)
     56       .cipherSuites(APPROVED_CIPHER_SUITES)
     57       .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
     58       .supportsTlsExtensions(true)
     59       .build();
     60 
     61   /** A backwards-compatible fallback connection for interop with obsolete servers. */
     62   public static final ConnectionSpec COMPATIBLE_TLS = new Builder(MODERN_TLS)
     63       .tlsVersions(TlsVersion.TLS_1_0)
     64       .supportsTlsExtensions(true)
     65       .build();
     66 
     67   /** Unencrypted, unauthenticated connections for {@code http:} URLs. */
     68   public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
     69 
     70   final boolean tls;
     71 
     72   /**
     73    * Used if tls == true. The cipher suites to set on the SSLSocket. {@code null} means "use
     74    * default set".
     75    */
     76   private final String[] cipherSuites;
     77 
     78   /** Used if tls == true. The TLS protocol versions to use. */
     79   private final String[] tlsVersions;
     80 
     81   final boolean supportsTlsExtensions;
     82 
     83   private ConnectionSpec(Builder builder) {
     84     this.tls = builder.tls;
     85     this.cipherSuites = builder.cipherSuites;
     86     this.tlsVersions = builder.tlsVersions;
     87     this.supportsTlsExtensions = builder.supportsTlsExtensions;
     88   }
     89 
     90   public boolean isTls() {
     91     return tls;
     92   }
     93 
     94   /**
     95    * Returns the cipher suites to use for a connection. This method can return {@code null} if the
     96    * cipher suites enabled by default should be used.
     97    */
     98   public List<CipherSuite> cipherSuites() {
     99     if (cipherSuites == null) {
    100       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   public List<TlsVersion> tlsVersions() {
    110     TlsVersion[] result = new TlsVersion[tlsVersions.length];
    111     for (int i = 0; i < tlsVersions.length; i++) {
    112       result[i] = TlsVersion.forJavaName(tlsVersions[i]);
    113     }
    114     return Util.immutableList(result);
    115   }
    116 
    117   public boolean supportsTlsExtensions() {
    118     return supportsTlsExtensions;
    119   }
    120 
    121   /** Applies this spec to {@code sslSocket}. */
    122   void apply(SSLSocket sslSocket, boolean isFallback) {
    123     ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
    124 
    125     sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    126 
    127     String[] cipherSuitesToEnable = specToApply.cipherSuites;
    128     // null means "use default set".
    129     if (cipherSuitesToEnable != null) {
    130       sslSocket.setEnabledCipherSuites(cipherSuitesToEnable);
    131     }
    132   }
    133 
    134   /**
    135    * Returns a copy of this that omits cipher suites and TLS versions not enabled by
    136    * {@code sslSocket}.
    137    */
    138   private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
    139     String[] cipherSuitesToEnable = null;
    140     if (cipherSuites != null) {
    141       String[] cipherSuitesToSelectFrom = sslSocket.getEnabledCipherSuites();
    142       cipherSuitesToEnable =
    143           Util.intersect(String.class, cipherSuites, cipherSuitesToSelectFrom);
    144     }
    145 
    146     if (isFallback) {
    147       // In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
    148       // the SCSV cipher is added to signal that a protocol fallback has taken place.
    149       final String fallbackScsv = "TLS_FALLBACK_SCSV";
    150       boolean socketSupportsFallbackScsv =
    151           Arrays.asList(sslSocket.getSupportedCipherSuites()).contains(fallbackScsv);
    152 
    153       if (socketSupportsFallbackScsv) {
    154         // Add the SCSV cipher to the set of enabled cipher suites iff it is supported.
    155         String[] oldEnabledCipherSuites = cipherSuitesToEnable != null
    156             ? cipherSuitesToEnable
    157             : sslSocket.getEnabledCipherSuites();
    158         String[] newEnabledCipherSuites = new String[oldEnabledCipherSuites.length + 1];
    159         System.arraycopy(oldEnabledCipherSuites, 0,
    160             newEnabledCipherSuites, 0, oldEnabledCipherSuites.length);
    161         newEnabledCipherSuites[newEnabledCipherSuites.length - 1] = fallbackScsv;
    162         cipherSuitesToEnable = newEnabledCipherSuites;
    163       }
    164     }
    165 
    166     String[] protocolsToSelectFrom = sslSocket.getEnabledProtocols();
    167     String[] protocolsToEnable = Util.intersect(String.class, tlsVersions, protocolsToSelectFrom);
    168     return new Builder(this)
    169         .cipherSuites(cipherSuitesToEnable)
    170         .tlsVersions(protocolsToEnable)
    171         .build();
    172   }
    173 
    174   /**
    175    * Returns {@code true} if the socket, as currently configured, supports this ConnectionSpec.
    176    * In order for a socket to be compatible the enabled cipher suites and protocols must intersect.
    177    *
    178    * <p>For cipher suites, at least one of the {@link #cipherSuites() required cipher suites} must
    179    * match the socket's enabled cipher suites. If there are no required cipher suites the socket
    180    * must have at least one cipher suite enabled.
    181    *
    182    * <p>For protocols, at least one of the {@link #tlsVersions() required protocols} must match the
    183    * socket's enabled protocols.
    184    */
    185   public boolean isCompatible(SSLSocket socket) {
    186     if (!tls) {
    187       return false;
    188     }
    189 
    190     String[] enabledProtocols = socket.getEnabledProtocols();
    191     boolean requiredProtocolsEnabled = nonEmptyIntersection(tlsVersions, enabledProtocols);
    192     if (!requiredProtocolsEnabled) {
    193       return false;
    194     }
    195 
    196     boolean requiredCiphersEnabled;
    197     if (cipherSuites == null) {
    198       requiredCiphersEnabled = socket.getEnabledCipherSuites().length > 0;
    199     } else {
    200       String[] enabledCipherSuites = socket.getEnabledCipherSuites();
    201       requiredCiphersEnabled = nonEmptyIntersection(cipherSuites, enabledCipherSuites);
    202     }
    203     return requiredCiphersEnabled;
    204   }
    205 
    206   /**
    207    * An N*M intersection that terminates if any intersection is found. The sizes of both
    208    * arguments are assumed to be so small, and the likelihood of an intersection so great, that it
    209    * is not worth the CPU cost of sorting or the memory cost of hashing.
    210    */
    211   private static boolean nonEmptyIntersection(String[] a, String[] b) {
    212     if (a == null || b == null || a.length == 0 || b.length == 0) {
    213       return false;
    214     }
    215     for (String toFind : a) {
    216       if (contains(b, toFind)) {
    217         return true;
    218       }
    219     }
    220     return false;
    221   }
    222 
    223   private static <T> boolean contains(T[] array, T value) {
    224     for (T arrayValue : array) {
    225       if (Util.equal(value, arrayValue)) {
    226         return true;
    227       }
    228     }
    229     return false;
    230   }
    231 
    232   @Override public boolean equals(Object other) {
    233     if (!(other instanceof ConnectionSpec)) return false;
    234     if (other == this) return true;
    235 
    236     ConnectionSpec that = (ConnectionSpec) other;
    237     if (this.tls != that.tls) return false;
    238 
    239     if (tls) {
    240       if (!Arrays.equals(this.cipherSuites, that.cipherSuites)) return false;
    241       if (!Arrays.equals(this.tlsVersions, that.tlsVersions)) return false;
    242       if (this.supportsTlsExtensions != that.supportsTlsExtensions) return false;
    243     }
    244 
    245     return true;
    246   }
    247 
    248   @Override public int hashCode() {
    249     int result = 17;
    250     if (tls) {
    251       result = 31 * result + Arrays.hashCode(cipherSuites);
    252       result = 31 * result + Arrays.hashCode(tlsVersions);
    253       result = 31 * result + (supportsTlsExtensions ? 0 : 1);
    254     }
    255     return result;
    256   }
    257 
    258   @Override public String toString() {
    259     if (tls) {
    260       List<CipherSuite> cipherSuites = cipherSuites();
    261       String cipherSuitesString = cipherSuites == null ? "[use default]" : cipherSuites.toString();
    262       return "ConnectionSpec(cipherSuites=" + cipherSuitesString
    263           + ", tlsVersions=" + tlsVersions()
    264           + ", supportsTlsExtensions=" + supportsTlsExtensions
    265           + ")";
    266     } else {
    267       return "ConnectionSpec()";
    268     }
    269   }
    270 
    271   public static final class Builder {
    272     private boolean tls;
    273     private String[] cipherSuites;
    274     private String[] tlsVersions;
    275     private boolean supportsTlsExtensions;
    276 
    277     Builder(boolean tls) {
    278       this.tls = tls;
    279     }
    280 
    281     public Builder(ConnectionSpec connectionSpec) {
    282       this.tls = connectionSpec.tls;
    283       this.cipherSuites = connectionSpec.cipherSuites;
    284       this.tlsVersions = connectionSpec.tlsVersions;
    285       this.supportsTlsExtensions = connectionSpec.supportsTlsExtensions;
    286     }
    287 
    288     public Builder cipherSuites(CipherSuite... cipherSuites) {
    289       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
    290 
    291       // Convert enums to the string names Java wants. This makes a defensive copy!
    292       String[] strings = new String[cipherSuites.length];
    293       for (int i = 0; i < cipherSuites.length; i++) {
    294         strings[i] = cipherSuites[i].javaName;
    295       }
    296       this.cipherSuites = strings;
    297       return this;
    298     }
    299 
    300     public Builder cipherSuites(String... cipherSuites) {
    301       if (!tls) throw new IllegalStateException("no cipher suites for cleartext connections");
    302 
    303       if (cipherSuites == null) {
    304         this.cipherSuites = null;
    305       } else {
    306         // This makes a defensive copy!
    307         this.cipherSuites = cipherSuites.clone();
    308       }
    309 
    310       return this;
    311     }
    312 
    313     public Builder tlsVersions(TlsVersion... tlsVersions) {
    314       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
    315       if (tlsVersions.length == 0) {
    316         throw new IllegalArgumentException("At least one TlsVersion is required");
    317       }
    318 
    319       // Convert enums to the string names Java wants. This makes a defensive copy!
    320       String[] strings = new String[tlsVersions.length];
    321       for (int i = 0; i < tlsVersions.length; i++) {
    322         strings[i] = tlsVersions[i].javaName;
    323       }
    324       this.tlsVersions = strings;
    325       return this;
    326     }
    327 
    328     public Builder tlsVersions(String... tlsVersions) {
    329       if (!tls) throw new IllegalStateException("no TLS versions for cleartext connections");
    330 
    331       if (tlsVersions == null) {
    332         this.tlsVersions = null;
    333       } else {
    334         // This makes a defensive copy!
    335         this.tlsVersions = tlsVersions.clone();
    336       }
    337 
    338       return this;
    339     }
    340 
    341     public Builder supportsTlsExtensions(boolean supportsTlsExtensions) {
    342       if (!tls) throw new IllegalStateException("no TLS extensions for cleartext connections");
    343       this.supportsTlsExtensions = supportsTlsExtensions;
    344       return this;
    345     }
    346 
    347     public ConnectionSpec build() {
    348       return new ConnectionSpec(this);
    349     }
    350   }
    351 }
    352