main.okhttp3.internal.connection.RealRoutePlanner.kt Maven / Gradle / Ivy
The 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.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
import okhttp3.internal.USER_AGENT
import okhttp3.internal.canReuseConnectionFor
import okhttp3.internal.closeQuietly
import okhttp3.internal.concurrent.TaskRunner
import okhttp3.internal.connection.RoutePlanner.Plan
import okhttp3.internal.platform.Platform
import okhttp3.internal.toHostHeader
class RealRoutePlanner(
private val taskRunner: TaskRunner,
private val connectionPool: RealConnectionPool,
private val readTimeoutMillis: Int,
private val writeTimeoutMillis: Int,
private val socketConnectTimeoutMillis: Int,
private val socketReadTimeoutMillis: Int,
private val pingIntervalMillis: Int,
private val retryOnConnectionFailure: Boolean,
private val fastFallback: Boolean,
override val address: Address,
private val routeDatabase: RouteDatabase,
private val connectionUser: ConnectionUser,
) : RoutePlanner {
private var routeSelection: RouteSelector.Selection? = null
private var routeSelector: RouteSelector? = null
private var nextRouteToTry: Route? = null
override val deferredPlans = ArrayDeque()
override fun isCanceled(): Boolean = connectionUser.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
// Attempt a deferred plan before new routes.
if (deferredPlans.isNotEmpty()) return deferredPlans.removeFirst()
// 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 = connectionUser.candidateConnection() ?: 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(connectionUser.doExtensiveHealthChecks())
var noNewExchangesEvent = false
val toClose: Socket? =
synchronized(candidate) {
when {
!healthy -> {
noNewExchangesEvent = !candidate.noNewExchanges
candidate.noNewExchanges = true
connectionUser.releaseConnectionNoEvents()
}
candidate.noNewExchanges || !sameHostAndPort(candidate.route().address.url) -> {
connectionUser.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 (connectionUser.candidateConnection() != null) {
check(toClose == null)
return ReusePlan(candidate)
}
// The call's connection was released.
toClose?.closeQuietly()
connectionUser.connectionReleased(candidate)
connectionUser.connectionConnectionReleased(candidate)
if (toClose != null) {
connectionUser.connectionConnectionClosed(candidate)
} else if (noNewExchangesEvent) {
connectionUser.noNewExchanges(candidate)
}
return null
}
/** Plans to make a new connection by deciding which route to try next. */
@Throws(IOException::class)
internal 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 = routeDatabase,
connectionUser = connectionUser,
fastFallback = fastFallback,
)
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 (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 =
connectionPool.callAcquirePooledConnection(
doExtensiveHealthChecks = connectionUser.doExtensiveHealthChecks(),
address = address,
connectionUser = connectionUser,
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()
}
connectionUser.connectionAcquired(result)
connectionUser.connectionConnectionAcquired(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(
taskRunner = taskRunner,
connectionPool = connectionPool,
readTimeoutMillis = readTimeoutMillis,
writeTimeoutMillis = writeTimeoutMillis,
socketConnectTimeoutMillis = socketConnectTimeoutMillis,
socketReadTimeoutMillis = socketReadTimeoutMillis,
pingIntervalMillis = pingIntervalMillis,
retryOnConnectionFailure = retryOnConnectionFailure,
user = connectionUser,
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", USER_AGENT)
.build()
val fakeAuthChallengeResponse =
Response.Builder()
.request(proxyConnectRequest)
.protocol(Protocol.HTTP_1_1)
.code(HttpURLConnection.HTTP_PROXY_AUTH)
.message("Preemptive Authenticate")
.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 (deferredPlans.isNotEmpty()) {
return true
}
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? {
return synchronized(connection) {
when {
connection.routeFailureCount != 0 -> null
// This route is still in use.
!connection.noNewExchanges -> null
!connection.route().address.url.canReuseConnectionFor(address.url) -> null
else -> connection.route()
}
}
}
override fun sameHostAndPort(url: HttpUrl): Boolean {
val routeUrl = address.url
return url.port == routeUrl.port && url.host == routeUrl.host
}
}