blob: ce0a71d8439fdadd573b3d9aec0648d944e705c5 [file] [log] [blame]
/*
* Copyright (C) 2012 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 com.squareup.okhttp.internal.http;
import com.squareup.okhttp.Address;
import com.squareup.okhttp.Connection;
import com.squareup.okhttp.ConnectionPool;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.Dns;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.net.ssl.SSLHandshakeException;
import static com.squareup.okhttp.internal.Util.getEffectivePort;
/**
* Selects routes to connect to an origin server. Each connection requires a
* choice of proxy server, IP address, and TLS mode. Connections may also be
* recycled.
*/
public final class RouteSelector {
/** Uses {@link com.squareup.okhttp.internal.Platform#enableTlsExtensions}. */
private static final int TLS_MODE_MODERN = 1;
/** Uses {@link com.squareup.okhttp.internal.Platform#supportTlsIntolerantServer}. */
private static final int TLS_MODE_COMPATIBLE = 0;
/** No TLS mode. */
private static final int TLS_MODE_NULL = -1;
private final Address address;
private final URI uri;
private final ProxySelector proxySelector;
private final ConnectionPool pool;
private final Dns dns;
private final Set<Route> failedRoutes;
/* The most recently attempted route. */
private Proxy lastProxy;
private InetSocketAddress lastInetSocketAddress;
/* State for negotiating the next proxy to use. */
private boolean hasNextProxy;
private Proxy userSpecifiedProxy;
private Iterator<Proxy> proxySelectorProxies;
/* State for negotiating the next InetSocketAddress to use. */
private InetAddress[] socketAddresses;
private int nextSocketAddressIndex;
private int socketPort;
/* State for negotiating the next TLS configuration */
private int nextTlsMode = TLS_MODE_NULL;
/* State for negotiating failed routes */
private final List<Route> postponedRoutes;
public RouteSelector(Address address, URI uri, ProxySelector proxySelector, ConnectionPool pool,
Dns dns, Set<Route> failedRoutes) {
this.address = address;
this.uri = uri;
this.proxySelector = proxySelector;
this.pool = pool;
this.dns = dns;
this.failedRoutes = failedRoutes;
this.postponedRoutes = new LinkedList<Route>();
resetNextProxy(uri, address.getProxy());
}
/**
* Returns true if there's another route to attempt. Every address has at
* least one route.
*/
public boolean hasNext() {
return hasNextTlsMode() || hasNextInetSocketAddress() || hasNextProxy() || hasNextPostponed();
}
/**
* Returns the next route address to attempt.
*
* @throws NoSuchElementException if there are no more routes to attempt.
*/
public Connection next() throws IOException {
// Always prefer pooled connections over new connections.
Connection pooled = pool.get(address);
if (pooled != null) {
return pooled;
}
// Compute the next route to attempt.
if (!hasNextTlsMode()) {
if (!hasNextInetSocketAddress()) {
if (!hasNextProxy()) {
if (!hasNextPostponed()) {
throw new NoSuchElementException();
}
return new Connection(nextPostponed());
}
lastProxy = nextProxy();
resetNextInetSocketAddress(lastProxy);
}
lastInetSocketAddress = nextInetSocketAddress();
resetNextTlsMode();
}
boolean modernTls = nextTlsMode() == TLS_MODE_MODERN;
Route route = new Route(address, lastProxy, lastInetSocketAddress, modernTls);
if (failedRoutes.contains(route)) {
postponedRoutes.add(route);
// We will only recurse in order to skip previously failed routes. They will be
// tried last.
return next();
}
return new Connection(route);
}
/**
* Clients should invoke this method when they encounter a connectivity
* failure on a connection returned by this route selector.
*/
public void connectFailed(Connection connection, IOException failure) {
Route failedRoute = connection.getRoute();
if (failedRoute.getProxy().type() != Proxy.Type.DIRECT && proxySelector != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
proxySelector.connectFailed(uri, failedRoute.getProxy().address(), failure);
}
failedRoutes.add(failedRoute);
if (!(failure instanceof SSLHandshakeException)) {
// If the problem was not related to SSL then it will also fail with
// a different Tls mode therefore we can be proactive about it.
failedRoutes.add(failedRoute.flipTlsMode());
}
}
/** Resets {@link #nextProxy} to the first option. */
private void resetNextProxy(URI uri, Proxy proxy) {
this.hasNextProxy = true; // This includes NO_PROXY!
if (proxy != null) {
this.userSpecifiedProxy = proxy;
} else {
List<Proxy> proxyList = proxySelector.select(uri);
if (proxyList != null) {
this.proxySelectorProxies = proxyList.iterator();
}
}
}
/** Returns true if there's another proxy to try. */
private boolean hasNextProxy() {
return hasNextProxy;
}
/** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
private Proxy nextProxy() {
// If the user specifies a proxy, try that and only that.
if (userSpecifiedProxy != null) {
hasNextProxy = false;
return userSpecifiedProxy;
}
// Try each of the ProxySelector choices until one connection succeeds. If none succeed
// then we'll try a direct connection below.
if (proxySelectorProxies != null) {
while (proxySelectorProxies.hasNext()) {
Proxy candidate = proxySelectorProxies.next();
if (candidate.type() != Proxy.Type.DIRECT) {
return candidate;
}
}
}
// Finally try a direct connection.
hasNextProxy = false;
return Proxy.NO_PROXY;
}
/** Resets {@link #nextInetSocketAddress} to the first option. */
private void resetNextInetSocketAddress(Proxy proxy) throws UnknownHostException {
socketAddresses = null; // Clear the addresses. Necessary if getAllByName() below throws!
String socketHost;
if (proxy.type() == Proxy.Type.DIRECT) {
socketHost = uri.getHost();
socketPort = getEffectivePort(uri);
} else {
SocketAddress proxyAddress = proxy.address();
if (!(proxyAddress instanceof InetSocketAddress)) {
throw new IllegalArgumentException(
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
socketHost = proxySocketAddress.getHostName();
socketPort = proxySocketAddress.getPort();
}
// Try each address for best behavior in mixed IPv4/IPv6 environments.
socketAddresses = dns.getAllByName(socketHost);
nextSocketAddressIndex = 0;
}
/** Returns true if there's another socket address to try. */
private boolean hasNextInetSocketAddress() {
return socketAddresses != null;
}
/** Returns the next socket address to try. */
private InetSocketAddress nextInetSocketAddress() throws UnknownHostException {
InetSocketAddress result =
new InetSocketAddress(socketAddresses[nextSocketAddressIndex++], socketPort);
if (nextSocketAddressIndex == socketAddresses.length) {
socketAddresses = null; // So that hasNextInetSocketAddress() returns false.
nextSocketAddressIndex = 0;
}
return result;
}
/** Resets {@link #nextTlsMode} to the first option. */
private void resetNextTlsMode() {
nextTlsMode = (address.getSslSocketFactory() != null) ? TLS_MODE_MODERN : TLS_MODE_COMPATIBLE;
}
/** Returns true if there's another TLS mode to try. */
private boolean hasNextTlsMode() {
return nextTlsMode != TLS_MODE_NULL;
}
/** Returns the next TLS mode to try. */
private int nextTlsMode() {
if (nextTlsMode == TLS_MODE_MODERN) {
nextTlsMode = TLS_MODE_COMPATIBLE;
return TLS_MODE_MODERN;
} else if (nextTlsMode == TLS_MODE_COMPATIBLE) {
nextTlsMode = TLS_MODE_NULL; // So that hasNextTlsMode() returns false.
return TLS_MODE_COMPATIBLE;
} else {
throw new AssertionError();
}
}
/** Returns true if there is another postponed route to try. */
private boolean hasNextPostponed() {
return !postponedRoutes.isEmpty();
}
/** Returns the next postponed route to try. */
private Route nextPostponed() {
return postponedRoutes.remove(0);
}
}