All Downloads are FREE. Search and download functionalities are using the official Maven repository.

jvmMain.okhttp3.internal.connection.RealRoutePlanner.kt Maven / Gradle / Ivy

There is a newer version: 5.0.0-alpha.14
Show newest version
/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package okhttp3.internal.connection

import java.io.IOException
import java.net.HttpURLConnection
import java.net.Socket
import java.net.UnknownServiceException
import okhttp3.Address
import okhttp3.ConnectionSpec
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import okhttp3.internal.EMPTY_RESPONSE
import okhttp3.internal.canReuseConnectionFor
import okhttp3.internal.closeQuietly
import okhttp3.internal.connection.RoutePlanner.Plan
import okhttp3.internal.http.RealInterceptorChain
import okhttp3.internal.platform.Platform
import okhttp3.internal.toHostHeader
import okhttp3.internal.userAgent

class RealRoutePlanner(
  private val client: OkHttpClient,
  override val address: Address,
  private val call: RealCall,
  chain: RealInterceptorChain,
) : RoutePlanner {
  private val doExtensiveHealthChecks = chain.request.method != "GET"

  private var routeSelection: RouteSelector.Selection? = null
  private var routeSelector: RouteSelector? = null
  private var nextRouteToTry: Route? = null

  override fun isCanceled(): Boolean = call.isCanceled()

  @Throws(IOException::class)
  override fun plan(): Plan {
    val reuseCallConnection = planReuseCallConnection()
    if (reuseCallConnection != null) return reuseCallConnection

    // Attempt to get a connection from the pool.
    val pooled1 = planReusePooledConnection()
    if (pooled1 != null) return pooled1

    // Do blocking calls to plan a route for a new connection.
    val connect = planConnect()

    // Now that we have a set of IP addresses, make another attempt at getting a connection from
    // the pool. We have a better chance of matching thanks to connection coalescing.
    val pooled2 = planReusePooledConnection(connect, connect.routes)
    if (pooled2 != null) return pooled2

    return connect
  }

  /**
   * Returns the connection already attached to the call if it's eligible for a new exchange.
   *
   * If the call's connection exists and is eligible for another exchange, it is returned. If it
   * exists but cannot be used for another exchange, it is closed and this returns null.
   */
  private fun planReuseCallConnection(): ReusePlan? {
    // This may be mutated by releaseConnectionNoEvents()!
    val candidate = call.connection ?: return null

    // Make sure this connection is healthy & eligible for new exchanges. If it's no longer needed
    // then we're on the hook to close it.
    val healthy = candidate.isHealthy(doExtensiveHealthChecks)
    val toClose: Socket? = synchronized(candidate) {
      when {
        !healthy -> {
          candidate.noNewExchanges = true
          call.releaseConnectionNoEvents()
        }
        candidate.noNewExchanges || !sameHostAndPort(candidate.route().address.url) -> {
          call.releaseConnectionNoEvents()
        }
        else -> null
      }
    }

    // If the call's connection wasn't released, reuse it. We don't call connectionAcquired() here
    // because we already acquired it.
    if (call.connection != null) {
      check(toClose == null)
      return ReusePlan(candidate)
    }

    // The call's connection was released.
    toClose?.closeQuietly()
    call.eventListener.connectionReleased(call, candidate)
    return null
  }

  /** Plans to make a new connection by deciding which route to try next. */
  @Throws(IOException::class)
  private fun planConnect(): ConnectPlan {
    // Use a route from a preceding coalesced connection.
    val localNextRouteToTry = nextRouteToTry
    if (localNextRouteToTry != null) {
      nextRouteToTry = null
      return planConnectToRoute(localNextRouteToTry)
    }

    // Use a route from an existing route selection.
    val existingRouteSelection = routeSelection
    if (existingRouteSelection != null && existingRouteSelection.hasNext()) {
      return planConnectToRoute(existingRouteSelection.next())
    }

    // Decide which proxy to use, if any. This may block in ProxySelector.select().
    var newRouteSelector = routeSelector
    if (newRouteSelector == null) {
      newRouteSelector = RouteSelector(
        address = address,
        routeDatabase = call.client.routeDatabase,
        call = call,
        fastFallback = client.fastFallback,
        eventListener = call.eventListener
      )
      routeSelector = newRouteSelector
    }

    // List available IP addresses for the current proxy. This may block in Dns.lookup().
    if (!newRouteSelector.hasNext()) throw IOException("exhausted all routes")
    val newRouteSelection = newRouteSelector.next()
    routeSelection = newRouteSelection

    if (call.isCanceled()) throw IOException("Canceled")

    return planConnectToRoute(newRouteSelection.next(), newRouteSelection.routes)
  }

  /**
   * Returns a plan to reuse a pooled connection, or null if the pool doesn't have a connection for
   * this address.
   *
   * If [planToReplace] is non-null, this will swap it for a pooled connection if that pooled
   * connection uses HTTP/2. That results in fewer sockets overall and thus fewer TCP slow starts.
   */
  internal fun planReusePooledConnection(
    planToReplace: ConnectPlan? = null,
    routes: List? = null,
  ): ReusePlan? {
    val result = client.connectionPool.delegate.callAcquirePooledConnection(
      doExtensiveHealthChecks = doExtensiveHealthChecks,
      address = address,
      call = call,
      routes = routes,
      requireMultiplexed = planToReplace != null && planToReplace.isReady
    ) ?: return null

    // If we coalesced our connection, remember the replaced connection's route. That way if the
    // coalesced connection later fails we don't waste a valid route.
    if (planToReplace != null) {
      nextRouteToTry = planToReplace.route
      planToReplace.closeQuietly()
    }

    call.eventListener.connectionAcquired(call, result)
    return ReusePlan(result)
  }

  /** Returns a plan for the first attempt at [route]. This throws if no plan is possible. */
  @Throws(IOException::class)
  internal fun planConnectToRoute(route: Route, routes: List? = null): ConnectPlan {
    if (route.address.sslSocketFactory == null) {
      if (ConnectionSpec.CLEARTEXT !in route.address.connectionSpecs) {
        throw UnknownServiceException("CLEARTEXT communication not enabled for client")
      }

      val host = route.address.url.host
      if (!Platform.get().isCleartextTrafficPermitted(host)) {
        throw UnknownServiceException(
          "CLEARTEXT communication to $host not permitted by network security policy"
        )
      }
    } else {
      if (Protocol.H2_PRIOR_KNOWLEDGE in route.address.protocols) {
        throw UnknownServiceException("H2_PRIOR_KNOWLEDGE cannot be used with HTTPS")
      }
    }

    val tunnelRequest = when {
      route.requiresTunnel() -> createTunnelRequest(route)
      else -> null
    }

    return ConnectPlan(
      client = client,
      call = call,
      routePlanner = this,
      route = route,
      routes = routes,
      attempt = 0,
      tunnelRequest = tunnelRequest,
      connectionSpecIndex = -1,
      isTlsFallback = false,
    )
  }

  /**
   * Returns a request that creates a TLS tunnel via an HTTP proxy. Everything in the tunnel request
   * is sent unencrypted to the proxy server, so tunnels include only the minimum set of headers.
   * This avoids sending potentially sensitive data like HTTP cookies to the proxy unencrypted.
   *
   * In order to support preemptive authentication we pass a fake "Auth Failed" response to the
   * authenticator. This gives the authenticator the option to customize the CONNECT request. It can
   * decline to do so by returning null, in which case OkHttp will use it as-is.
   */
  @Throws(IOException::class)
  private fun createTunnelRequest(route: Route): Request {
    val proxyConnectRequest = Request.Builder()
      .url(route.address.url)
      .method("CONNECT", null)
      .header("Host", route.address.url.toHostHeader(includeDefaultPort = true))
      .header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
      .header("User-Agent", userAgent)
      .build()

    val fakeAuthChallengeResponse = Response.Builder()
      .request(proxyConnectRequest)
      .protocol(Protocol.HTTP_1_1)
      .code(HttpURLConnection.HTTP_PROXY_AUTH)
      .message("Preemptive Authenticate")
      .body(EMPTY_RESPONSE)
      .sentRequestAtMillis(-1L)
      .receivedResponseAtMillis(-1L)
      .header("Proxy-Authenticate", "OkHttp-Preemptive")
      .build()

    val authenticatedRequest = route.address.proxyAuthenticator
      .authenticate(route, fakeAuthChallengeResponse)

    return authenticatedRequest ?: proxyConnectRequest
  }

  override fun hasNext(failedConnection: RealConnection?): Boolean {
    if (nextRouteToTry != null) {
      return true
    }

    if (failedConnection != null) {
      val retryRoute = retryRoute(failedConnection)
      if (retryRoute != null) {
        // Lock in the route because retryRoute() is racy and we don't want to call it twice.
        nextRouteToTry = retryRoute
        return true
      }
    }

    // If we have a routes left, use 'em.
    if (routeSelection?.hasNext() == true) return true

    // If we haven't initialized the route selector yet, assume it'll have at least one route.
    val localRouteSelector = routeSelector ?: return true

    // If we do have a route selector, use its routes.
    return localRouteSelector.hasNext()
  }

  /**
   * Return the route from [connection] if it should be retried, even if the connection itself is
   * unhealthy. The biggest gotcha here is that we shouldn't reuse routes from coalesced
   * connections.
   */
  private fun retryRoute(connection: RealConnection): Route? {
    synchronized(connection) {
      if (connection.routeFailureCount != 0) return null
      if (!connection.noNewExchanges) return null // This route is still in use.
      if (!connection.route().address.url.canReuseConnectionFor(address.url)) return null
      return connection.route()
    }
  }

  override fun sameHostAndPort(url: HttpUrl): Boolean {
    val routeUrl = address.url
    return url.port == routeUrl.port && url.host == routeUrl.host
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy