ConnectionSpec

2019-11-25  本文已影响0人  嗯哼嗯哼嗯哼嗯哼

OkHttp

okhttp是Android 平台的网络请求框架,已经被Android吸收了。并且OkHttp很好的支持了Cookie,Cache,连接复用,HTTP2 Https...使用拦截链模式,能很方便的扩展自己的功能。从Android4.4开始已经引入了OkHttp

ConnectionSpec

ConnectionSpec是用来指定Http传输时的Socket连接。对于Https连接,ConnectionSpec是在构建TLS连接时向服务端说明客户端支持的TLS版本,密码套件等的一个类。
下面介绍一下OkHttp默认的ConnectionSpec

// Most secure but generally supported list.  支持的密码套件的列表
  private static final CipherSuite[] RESTRICTED_CIPHER_SUITES = new CipherSuite[] {
      // TLSv1.3
      CipherSuite.TLS_AES_128_GCM_SHA256,
      CipherSuite.TLS_AES_256_GCM_SHA384,
      CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
      CipherSuite.TLS_AES_128_CCM_SHA256,
      CipherSuite.TLS_AES_256_CCM_8_SHA256,

      CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
      CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
  };

//支持的密码套件的列表
  // This is nearly equal to the cipher suites supported in Chrome 51, current as of 2016-05-25.
  // All of these suites are available on Android 7.0; earlier releases support a subset of these
  // suites. https://github.com/square/okhttp/issues/1972
  private static final CipherSuite[] APPROVED_CIPHER_SUITES = new CipherSuite[] {
      // TLSv1.3
      CipherSuite.TLS_AES_128_GCM_SHA256,
      CipherSuite.TLS_AES_256_GCM_SHA384,
      CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
      CipherSuite.TLS_AES_128_CCM_SHA256,
      CipherSuite.TLS_AES_256_CCM_8_SHA256,

      CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
      CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_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_RSA_WITH_AES_128_CBC_SHA,
      CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
      CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
      CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
      CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
      CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
      CipherSuite.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
  };

  //安全的TLS连接需要最近的客户端和最近的服务器
  public static final ConnectionSpec RESTRICTED_TLS = new Builder(true)
      .cipherSuites(RESTRICTED_CIPHER_SUITES)
      .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2)
      .supportsTlsExtensions(true)
      .build();

  //现代的TLS的配置,适用于大多数客户端和可以连接到的服务器,是OkHttp的默认配置
  public static final ConnectionSpec MODERN_TLS = new Builder(true)
      .cipherSuites(APPROVED_CIPHER_SUITES)
      .tlsVersions(TlsVersion.TLS_1_3, TlsVersion.TLS_1_2, TlsVersion.TLS_1_1, TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

  //向后兼容的配置,相比于MODERN_TLS,支持的TLS的版本变少了,只支持TLS_1_0版本
  public static final ConnectionSpec COMPATIBLE_TLS = new Builder(true)
      .cipherSuites(APPROVED_CIPHER_SUITES)
      .tlsVersions(TlsVersion.TLS_1_0)
      .supportsTlsExtensions(true)
      .build();

  //未加密未认证的连接,就是HTTP连接
  public static final ConnectionSpec CLEARTEXT = new Builder(false).build();
  

上面是OkHttp提供的一些默认的ConnectionSpec,再找下OKHttpClient中默认用的是哪些ConnectionSpec

static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS = Util.immutableList(
      ConnectionSpec.MODERN_TLS, ConnectionSpec.CLEARTEXT);
      
      
public Builder() {
    ......
    protocols = DEFAULT_PROTOCOLS;
    connectionSpecs = DEFAULT_CONNECTION_SPECS;
    ...
}

从代码中看的出来,默认ConnectionSpec.MODERN_TLS(支持较多的TLS版本和较多的密码套件), ConnectionSpec.CLEARTEXT(支持HTTP连接)

下面再拆解ConnectionSpec

final boolean tls;//是否是TLS连接
final boolean supportsTlsExtensions;//是否支持Tls扩展
final @Nullable String[] cipherSuites;//支持的密码套件类型
final @Nullable String[] tlsVersions;//支持的Tls版本的类型
  
  
 //把相应的ConnectionSpec设置到SSLSocket上面,使其具有相应的密码套件和TLS版本的支持
 //isFallback 是否支持回退策略,就是是否还支持其他的密码套件
void apply(SSLSocket sslSocket, boolean isFallback) {
    ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);//返回这个socket支持的ConnectionSpec,并在设置到SSLSocket上
    
    if (specToApply.tlsVersions != null) {
      sslSocket.setEnabledProtocols(specToApply.tlsVersions);
    }
    if (specToApply.cipherSuites != null) {
      sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
    }
    
}

//返回sslSocket支持的密码套件和TLS版本
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
    String[] cipherSuitesIntersection = cipherSuites != null
        ? intersect(CipherSuite.ORDER_BY_NAME, sslSocket.getEnabledCipherSuites(), cipherSuites)
        : sslSocket.getEnabledCipherSuites();
    String[] tlsVersionsIntersection = tlsVersions != null
        ? intersect(Util.NATURAL_ORDER, sslSocket.getEnabledProtocols(), tlsVersions)
        : 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.
    String[] supportedCipherSuites = sslSocket.getSupportedCipherSuites();
    int indexOfFallbackScsv = indexOf(
        CipherSuite.ORDER_BY_NAME, supportedCipherSuites, "TLS_FALLBACK_SCSV");
    if (isFallback && indexOfFallbackScsv != -1) {
      cipherSuitesIntersection = concat(
          cipherSuitesIntersection, supportedCipherSuites[indexOfFallbackScsv]);
    }
    
    return new Builder(this)
        .cipherSuites(cipherSuitesIntersection)
        .tlsVersions(tlsVersionsIntersection)
        .build();//构造出ConnectionSpec
}
  
//是否与传入的SSLSocket具有匹配的密码套件和TLS版本  
public boolean isCompatible(SSLSocket socket) {
    if (!tls) {
      return false;
    }
    
    if (tlsVersions != null && !nonEmptyIntersection(
        Util.NATURAL_ORDER, tlsVersions, socket.getEnabledProtocols())) {
      return false;
    }
    
    if (cipherSuites != null && !nonEmptyIntersection(
        CipherSuite.ORDER_BY_NAME, cipherSuites, socket.getEnabledCipherSuites())) {
      return false;
    }
    
    return true;
}

  

调用Apply方法的入口地址是在RealConnection的connectTls()方法中

确定这个socket简历TLS连接时的对应的密码套件和TLS版本
// Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);

ConnectionSpecSelector

ConnectionSpecSelector是处理ConnectionSpec的后备策略,当Https由于握手或者TLS版本,密码套件问题连接失败时,可以尝试使用不同的协议。ConnectionSpecSelector是有状态的,所以对于每一个Connection都应该创建一个新的ConnectionSpecSelector

private final List<ConnectionSpec> connectionSpecs;//支持的ConnectionSpec列表
private int nextModeIndex;//当前可用的协议在列表中的位置的下一个索引(便于查找是否支持后备策略)
private boolean isFallbackPossible;//是否支持后备策略
private boolean isFallback;//是否处于后备策略

ConnectionSpecSelector比较简单里面的方法有

    public ConnectionSpecSelector(List<ConnectionSpec> connectionSpecs) {
    this.nextModeIndex = 0;
    this.connectionSpecs = connectionSpecs;
  }

  public ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
    ConnectionSpec tlsConfiguration = null;
    for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
      ConnectionSpec connectionSpec = connectionSpecs.get(i);
      if (connectionSpec.isCompatible(sslSocket)) {
        tlsConfiguration = connectionSpec;
        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 UnknownServiceException(
          "Unable to find acceptable protocols. isFallback=" + isFallback
              + ", modes=" + connectionSpecs
              + ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
    }

    isFallbackPossible = isFallbackPossible(sslSocket);

    Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);

    return tlsConfiguration;
  }

//是否支持后备策略,从nextModeIndex处开始遍历,判断后面的ConnectionSpec是否匹配SSLSocket
  private boolean isFallbackPossible(SSLSocket socket) {
    for (int i = nextModeIndex; i < connectionSpecs.size(); i++) {
      if (connectionSpecs.get(i).isCompatible(socket)) {
        return true;
      }
    }
    return false;
  }
  1. 构造方法中nextModeIndex为0,遍历connectionSpec列表,找到适合SSLSocket的ConnectionSpec,如果未找到直接抛出异常
  2. 判断在ConnectionSpec列表的后面是否还有适合SSLSocket的connectionSpec,如果有的话,那么就代表支持支持后备策略isFallbackPossible为true
  3. 将支持的ConnectionSpec设置给SSLSocket对象,并且此时isFallback还是为isFallback,表明目前还未处于后备策略中

  public boolean connectionFailed(IOException e) {
    // Any future attempt to connect using this strategy will be a fallback attempt.
    isFallback = true;

    if (!isFallbackPossible) {
      return false;
    }

    // If there was a protocol problem, don't recover.
    if (e instanceof ProtocolException) {
      return false;
    }

    // If there was an interruption or timeout (SocketTimeoutException), don't recover.
    // For the socket connect timeout case we do not try the same host with a different
    // ConnectionSpec: we assume it is unreachable.
    if (e instanceof InterruptedIOException) {
      return false;
    }

    // Look for known client-side or negotiation errors that are unlikely to be fixed by trying
    // again with a different connection spec.
    if (e instanceof SSLHandshakeException) {
      // If the problem was a CertificateException from the X509TrustManager,
      // do not retry.
      if (e.getCause() instanceof CertificateException) {
        return false;
      }
    }
    if (e instanceof SSLPeerUnverifiedException) {
      // e.g. a certificate pinning error.
      return false;
    }

    // On Android, 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
        || e instanceof SSLException);
  }
  1. 如果之前的SSLSocket连接失败了,那么之后连接尝试都属于后备策略,所以isFallBack置为true

上面是分别介绍了ConnectionSpec和ConnectionSpecSelector,下面把这两个类直接串联起来,看看OkHttp里面是怎么使用的

在RealConnection中


public void connect(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
      EventListener eventListener) {
    ...
    List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
    ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);//

    if (route.address().sslSocketFactory() == null) {
      if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication not enabled for client"));
      }
      String host = route.address().url().host();
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw new RouteException(new UnknownServiceException(
            "CLEARTEXT communication to " + host + " not permitted by network security policy"));
      }
    } else {
      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
        throw new RouteException(new UnknownServiceException(
            "H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
      }
    }

    while (true) {
      try {
        if (route.requiresTunnel()) {
          connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
          if (rawSocket == null) {
            // We were unable to connect the tunnel but properly closed down our resources.
            break;
          }
        } else {
          connectSocket(connectTimeout, readTimeout, call, eventListener);
        }
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
        eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
        break;
      } catch (IOException e) {
        ...
        //这里的connectionRetryEnabled默认值为true,是从OKHttpClient中传过来的,默认值为true
        //所以这里会执行ConnectionSpecSelector的connectionFailed()方法,如果是SSLHandshakeException,SSLProtocolException,SSLException那么就会执行后备策略,继续尝试TLS连接
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException;
        }
      }
    }

    ...
  }

  1. Address里面的ConnectionSpec就是OkHttpClient传入的ConnectionSpec,也就是之前说到的默认值
  2. 通过ConnectionSpecs构造出ConnectionSpecSelector
  3. 对于HTTP连接,如果OkHttp和Platform不支持Http的话,那么就抛出异常,Platform在Android
    上是Android Platform
  4. 开始尝试连接了,connectSocket主要是构建TCP连接,establishProtocol方法里面会构建TLS连接,最终会走到connectTls方法
  5. 如果连接过程失败了,那么会调用connectionSpecSelector的connectionFailed(),如果异常是SSLHandshakeException,SSLProtocolException,SSLException那么就会执行后备策略,继续尝试TLS连接,走回while(true)

下面再具体分析connectTls()方法,开始Tls连接

private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
    Address address = route.address();
    SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
    boolean success = false;
    SSLSocket sslSocket = null;
    try {
      // Create the wrapper over the connected socket.
      sslSocket = (SSLSocket) sslSocketFactory.createSocket(
          rawSocket, address.url().host(), address.url().port(), true /* autoClose */);


      // Configure the socket's ciphers, TLS versions, and extensions.
      ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
      if (connectionSpec.supportsTlsExtensions()) {
        Platform.get().configureTlsExtensions(
            sslSocket, address.url().host(), address.protocols());
      }

      // Force handshake. This can throw!
      sslSocket.startHandshake();
      // block for session establishment
      SSLSession sslSocketSession = sslSocket.getSession();
      Handshake unverifiedHandshake = Handshake.get(sslSocketSession);

      // Verify that the socket's certificates are acceptable for the target host.
      if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
        X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
        throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
            + "\n    certificate: " + CertificatePinner.pin(cert)
            + "\n    DN: " + cert.getSubjectDN().getName()
            + "\n    subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
      }

      // Check that the certificate pinner is satisfied by the certificates presented.
      address.certificatePinner().check(address.url().host(),
          unverifiedHandshake.peerCertificates());

      // Success! Save the handshake and the ALPN protocol.
      String maybeProtocol = connectionSpec.supportsTlsExtensions()
          ? Platform.get().getSelectedProtocol(sslSocket)
          : null;
      socket = sslSocket;
      source = Okio.buffer(Okio.source(socket));
      sink = Okio.buffer(Okio.sink(socket));
      handshake = unverifiedHandshake;
      protocol = maybeProtocol != null
          ? Protocol.get(maybeProtocol)
          : Protocol.HTTP_1_1;
      success = true;
    } catch (AssertionError e) {
      if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
      throw e;
    } finally {
      if (sslSocket != null) {
        Platform.get().afterHandshake(sslSocket);
      }
      if (!success) {
        closeQuietly(sslSocket);
      }
    }
  }
  1. connectionSpecSelector.configureSecureSocket(sslSocket);开始确认密码套件,Tls版本和tls扩展等,这里就会调用ConnectionSpecSelector的方法,这里就是入口,这样就串联起来了,可以从这里进入,再结合上面对两个类的分析,再梳理下流程
上一篇下一篇

猜你喜欢

热点阅读