270 lines
9.0 KiB
Java
270 lines
9.0 KiB
Java
/*
|
|
* 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.RouteDatabase;
|
|
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 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 RouteDatabase routeDatabase;
|
|
|
|
/* 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, RouteDatabase routeDatabase) {
|
|
this.address = address;
|
|
this.uri = uri;
|
|
this.proxySelector = proxySelector;
|
|
this.pool = pool;
|
|
this.dns = dns;
|
|
this.routeDatabase = routeDatabase;
|
|
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(String method) throws IOException {
|
|
// Always prefer pooled connections over new connections.
|
|
for (Connection pooled; (pooled = pool.get(address)) != null; ) {
|
|
if (method.equals("GET") || pooled.isReadable()) return pooled;
|
|
pooled.close();
|
|
}
|
|
|
|
// 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 (routeDatabase.shouldPostpone(route)) {
|
|
postponedRoutes.add(route);
|
|
// We will only recurse in order to skip previously failed routes. They will be
|
|
// tried last.
|
|
return next(method);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
routeDatabase.failed(failedRoute, failure);
|
|
}
|
|
|
|
/** 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);
|
|
}
|
|
}
|