RealConnection#connect()疑似无限重试问题

4 阅读2分钟

RealConnection#connect()疑似无限重试问题

connectionRetryEnabled

通过okhttpClient.retryOnConnectionFailure设置为true

ConnectionSpecSelector#isFallbackPossible

  • 初始为false
  • 调用configureSecureSocket()方法对isFallbackPossible赋值,可能为true或者false。

establishProtocol()

这个方法有如下调用路径:

establishProtocol() -> connectTls() -> |configureSecureSocket()
                                       |...
                                       |sslSocket.startHandshake()                                       

可以看出,当establishProtocol()调用时,会对isFallbackPossible进行赋值。

connectionFailed(e)

  fun connectionFailed(e: IOException): Boolean {
    // Any future attempt to connect using this strategy will be a fallback attempt.
    isFallback = true

    return when {
        !isFallbackPossible -> false

        // If there was a protocol problem, don't recover.
        e is ProtocolException -> 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.
        e is InterruptedIOException -> false

        // If the problem was a CertificateException from the X509TrustManager, do not retry.
        e is SSLHandshakeException && e.cause is CertificateException -> false

        // e.g. a certificate pinning error.
        e is SSLPeerUnverifiedException -> false

        // Retry for all other SSL failures.
        e is SSLException -> true

        else -> false
    }
}

当isFallbackPossible == true 且e is SSLException == true时,connectionFailed()返回true

connection()方法疑似无限循环的流程


  fun connect(
    connectTimeout: Int,
    readTimeout: Int,
    writeTimeout: Int,
    pingIntervalMillis: Int,
    connectionRetryEnabled: Boolean,
    call: Call,
    eventListener: EventListener
  ) { 
    //..

    while (true) {
      try {
        if (route.requiresTunnel()) {
          // A  
          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)
        }
          
        // B  
        establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener)
        eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)
        
        // C  
        break
      } catch (e: IOException) {
        //...

        if (routeException == null) {
          routeException = RouteException(e)
        } else {
          routeException.addConnectException(e)
        }

        // D 
        if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
          throw routeException
        }
      }
    }

    //...
  }

前置:

下面讨论抛出SSLExcetpion时,指的是当isFallbackPossible == true时,这个SSLException需要能让connectionFailed()进入返回true的分支。

首次循环:

  1. 执行到establishProtocol(),isFallbackPossible被赋值为true
  2. 在isFallbackPossible赋值以后到// C之前抛出SSLException;
  3. 进入connectionFailed(e),根据isFallbackPossible和异常的值,这里返回true
  4. 于是进入第二次循环

第二次循环:

  1. 执行到connectTunnel()抛出SSLExcetpion
  2. 跳过establishProtocol()执行,于是isFallbackPossible当值保持不变,为true
  3. 进入connectionFailed(e),根据isFallbackPossible和异常的值,这里返回true

问题出现:

如果第二次循环的场景重复出现,connect()方法就会进入无限循环。

connectTunnel()抛出SSLException的可能性

connectTunnel()方法内部调用中,可能抛出SSLException的点大多在库的调用者实现的代码中。例如:

//RealConnection#connectSocket()


/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
@Throws(IOException::class)
private fun connectSocket(
    connectTimeout: Int,
    readTimeout: Int,
    call: Call,
    eventListener: EventListener
) {
    val proxy = route.proxy
    val address = route.address

    val rawSocket = when (proxy.type()) {
        Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!// 可能的抛出点
        else -> Socket(proxy)
    }
    this.rawSocket = rawSocket

    // 可能的抛出点
    eventListener.connectStart(call, route.socketAddress, proxy)
    rawSocket.soTimeout = readTimeout

//....
}

在isFallbackPossible赋值以后到// C之前抛出SSLException的可能性

疑似点,ai说可能但是不确定:

例如:

//RealConnection#connectTls()


@Throws(IOException::class)
private fun connectTls(connectionSpecSelector: ConnectionSpecSelector) {
    val address = route.address
    val sslSocketFactory = address.sslSocketFactory
    var success = false
    var sslSocket: SSLSocket? = null
    try {
        // Create the wrapper over the connected socket.
        sslSocket = sslSocketFactory!!.createSocket(
            rawSocket, address.url.host, address.url.port, true /* autoClose */) as SSLSocket

        // Configure the socket's ciphers, TLS versions, and extensions.
        val 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
        val sslSocketSession = sslSocket.session
        val unverifiedHandshake = sslSocketSession.handshake()

        //..

        val certificatePinner = address.certificatePinner!!

        handshake = Handshake(unverifiedHandshake.tlsVersion, unverifiedHandshake.cipherSuite,
            unverifiedHandshake.localCertificates) {
            certificatePinner.certificateChainCleaner!!.clean(unverifiedHandshake.peerCertificates,
                address.url.host)
        }

        //..

        // Success! Save the handshake and the ALPN protocol.
        val maybeProtocol = if (connectionSpec.supportsTlsExtensions) {
            // 疑似
            Platform.get().getSelectedProtocol(sslSocket)
        } else {
            null
        }
       //..
    } finally {
        if (sslSocket != null) {
            // 疑似
            Platform.get().afterHandshake(sslSocket)
        }
       //..
    }
}

库的调用者实现的代码

例如:

// RealConnection#connect()

eventListener.connectEnd(call, route.socketAddress, route.proxy, protocol)