美文网首页
ConnectionSpec

ConnectionSpec

作者: 嗯哼嗯哼嗯哼嗯哼 | 来源:发表于2019-11-25 11:19 被阅读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的方法,这里就是入口,这样就串联起来了,可以从这里进入,再结合上面对两个类的分析,再梳理下流程

    相关文章

      网友评论

          本文标题:ConnectionSpec

          本文链接:https://www.haomeiwen.com/subject/zcpdwctx.html