proxy chains in HttpRoute/RouteTracker/RouteDirector

git-svn-id: https://svn.apache.org/repos/asf/jakarta/httpcomponents/httpclient/trunk@542193 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Roland Weber 2007-05-28 10:50:28 +00:00
parent 942bff97c5
commit d0a973d3bf
5 changed files with 377 additions and 79 deletions

View File

@ -50,7 +50,7 @@ import org.apache.http.util.CharArrayBuffer;
*
* @since 4.0
*/
public final class HttpRoute {
public final class HttpRoute implements Cloneable {
/** The target host to connect to. */
private final HttpHost targetHost;
@ -61,8 +61,8 @@ public final class HttpRoute {
*/
private final InetAddress localAddress;
/** The proxy server, if any. */
private final HttpHost proxyHost;
/** The proxy servers, if any. */
private final HttpHost[] proxyChain;
/** Whether the the route is tunnelled through the proxy. */
private final boolean tunnelled;
@ -74,12 +74,77 @@ public final class HttpRoute {
private final boolean secure;
/**
* Internal, fully-specified constructor.
* This constructor does <i>not</i> clone the proxy chain array,
* nor test it for <code>null</code> elements. This conversion and
* check is the responsibility of the public constructors.
* The order of arguments here is different from the similar public
* constructor, as required by Java.
*
* @param local the local address to route from, or
* <code>null</code> for the default
* @param target the host to which to route
* @param proxies the proxy chain to use, or
* <code>null</code> for a direct route
* @param secure <code>true</code> if the route is (to be) secure,
* <code>false</code> otherwise
* @param tunnelled <code>true</code> if the route is (to be) tunnelled
* end-to-end via the proxy chain,
* <code>false</code> otherwise
* @param layered <code>true</code> if the route includes a
* layered protocol,
* <code>false</code> otherwise
*/
private HttpRoute(InetAddress local, HttpHost target, HttpHost[] proxies,
boolean secure, boolean tunnelled, boolean layered) {
if (target == null) {
throw new IllegalArgumentException
("Target host may not be null.");
}
if (tunnelled && (proxies == null)) {
throw new IllegalArgumentException
("Proxy required if tunnelled.");
}
this.targetHost = target;
this.localAddress = local;
this.proxyChain = proxies;
this.secure = secure;
this.tunnelled = tunnelled;
this.layered = layered;
}
/**
* Creates a new route with all attributes specified explicitly.
*
* @param target the host to which to route
* @param local the local address to route from, or
* <code>null</code> for the default
* @param proxies the proxy chain to use, or
* <code>null</code> for a direct route
* @param secure <code>true</code> if the route is (to be) secure,
* <code>false</code> otherwise
* @param tunnelled <code>true</code> if the route is (to be) tunnelled
* end-to-end via the proxy chain,
* <code>false</code> otherwise
* @param layered <code>true</code> if the route includes a
* layered protocol,
* <code>false</code> otherwise
*/
public HttpRoute(HttpHost target, InetAddress local, HttpHost[] proxies,
boolean secure, boolean tunnelled, boolean layered) {
this(local, target, toChain(proxies), secure, tunnelled, layered);
}
/**
* Creates a new route with at most one proxy.
*
* @param target the host to which to route
* @param local the local address to route from, or
* <code>null</code> for the default
* @param proxy the proxy to use, or
* <code>null</code> for a direct route
* @param secure <code>true</code> if the route is (to be) secure,
@ -93,21 +158,7 @@ public final class HttpRoute {
*/
public HttpRoute(HttpHost target, InetAddress local, HttpHost proxy,
boolean secure, boolean tunnelled, boolean layered) {
if (target == null) {
throw new IllegalArgumentException
("Target host may not be null.");
}
if (tunnelled && (proxy == null)) {
throw new IllegalArgumentException
("Proxy host may not be null if tunnelled.");
}
this.targetHost = target;
this.localAddress = local;
this.proxyHost = proxy;
this.secure = secure;
this.tunnelled = tunnelled;
this.layered = layered;
this(local, target, toChain(proxy), secure, tunnelled, layered);
}
@ -122,18 +173,17 @@ public final class HttpRoute {
* <code>false</code> otherwise
*/
public HttpRoute(HttpHost target, InetAddress local, boolean secure) {
this(target, local, null, secure, false, false);
this(local, target, null, secure, false, false);
}
/**
* Creates a new direct insecure route.
* That is a route without a proxy.
*
* @param target the host to which to route
*/
public HttpRoute(HttpHost target) {
this(target, null, null, false, false, false);
this(null, target, null, false, false, false);
}
@ -152,7 +202,7 @@ public final class HttpRoute {
*/
public HttpRoute(HttpHost target, InetAddress local, HttpHost proxy,
boolean secure) {
this(target, local, proxy, secure, secure, secure);
this(local, target, toChain(proxy), secure, secure, secure);
if (proxy == null) {
throw new IllegalArgumentException
("Proxy host may not be null.");
@ -160,6 +210,47 @@ public final class HttpRoute {
}
/**
* Helper to convert a proxy to a proxy chain.
*
* @param proxy the only proxy in the chain, or <code>null</code>
*
* @return a proxy chain array, or <code>null</code>
*/
private static HttpHost[] toChain(HttpHost proxy) {
if (proxy == null)
return null;
return new HttpHost[]{ proxy };
}
/**
* Helper to duplicate and check a proxy chain.
* An empty proxy chain is converted to <code>null</code>.
*
* @param proxies the proxy chain to duplicate, or <code>null</code>
*
* @return a new proxy chain array, or <code>null</code>
*/
private static HttpHost[] toChain(HttpHost[] proxies) {
if ((proxies == null) || (proxies.length < 1))
return null;
for (int i=0; i<proxies.length; i++) {
if (proxies[i] == null)
throw new IllegalArgumentException
("Proxy chain may not contain null elements.");
}
// copy the proxy chain, the traditional way
HttpHost[] result = new HttpHost[proxies.length];
System.arraycopy(proxies, 0, result, 0, proxies.length);
return result;
}
/**
* Obtains the target host.
*
@ -182,20 +273,71 @@ public final class HttpRoute {
/**
* Obtains the proxy host.
* Obtains the number of hops in this route.
* A direct route has one hop. A route through a proxy has two hops.
* A route through a chain of <i>n</i> proxies has <i>n+1</i> hops.
*
* @return the number of hops in this route
*/
public final int getHopCount() {
return (proxyChain == null) ? 1 : (proxyChain.length+1);
}
/**
* Obtains the target of a hop in this route.
* The target of the last hop is the {@link #getTargetHost target host},
* the target of previous hops is the respective proxy in the chain.
* For a route through exactly one proxy, target of hop 0 is the proxy
* and target of hop 1 is the target host.
*
* @param hop index of the hop for which to get the target,
* 0 for first
*
* @return the target of the given hop
*
* @throws IllegalArgumentException
* if the argument is negative or not less than
* {@link #getHopCount getHopCount()}
*/
public final HttpHost getHopTarget(int hop) {
if (hop < 0)
throw new IllegalArgumentException
("Hop index must not be negative: " + hop);
if (((this.proxyChain == null) && (hop > 0)) ||
( this.proxyChain.length < hop)) {
throw new IllegalArgumentException
("Hop index " + hop +
" exceeds route length " + getHopCount() +".");
}
HttpHost result = null;
if ((this.proxyChain != null) && (hop < this.proxyChain.length))
result = this.proxyChain[hop];
else
result = this.targetHost;
return result;
}
/**
* Obtains the first proxy host.
*
* @return the proxy host, or
* @return the first proxy in the proxy chain, or
* <code>null</code> if this route is direct
*/
public final HttpHost getProxyHost() {
return this.proxyHost;
return (this.proxyChain == null) ? null : this.proxyChain[0];
}
/**
* Checks whether this route is tunnelled through a proxy.
* If there is a proxy chain, only end-to-end tunnels are considered.
*
* @return <code>true</code> if tunnelled,
* @return <code>true</code> if tunnelled end-to-end through at least
* one proxy,
* <code>false</code> otherwise
*/
public final boolean isTunnelled() {
@ -205,6 +347,8 @@ public final class HttpRoute {
/**
* Checks whether this route includes a layered protocol.
* In the presence of proxies, only layering over an end-to-end tunnel
* is considered.
*
* @return <code>true</code> if layered,
* <code>false</code> otherwise
@ -235,8 +379,11 @@ public final class HttpRoute {
* where routes need to be represented. No conversion necessary.
*/
public final HostConfiguration toHostConfig() {
if ((this.proxyChain != null) && (this.proxyChain.length > 1)) {
throw new IllegalStateException("Cannot convert proxy chain.");
}
return new HostConfiguration
(this.targetHost, this.proxyHost, this.localAddress);
(this.targetHost, getProxyHost(), this.localAddress);
}
@ -257,18 +404,25 @@ public final class HttpRoute {
HttpRoute that = (HttpRoute) o;
boolean equal = this.targetHost.equals(that.targetHost);
equal &=
( this.localAddress == that.localAddress) |
( this.localAddress == that.localAddress) ||
((this.localAddress != null) &&
this.localAddress.equals(that.localAddress));
equal &=
( this.proxyHost == that.proxyHost) |
((this.proxyHost != null) &&
this.proxyHost.equals(that.proxyHost));
( this.proxyChain == that.proxyChain) ||
((this.proxyChain != null) &&
(this.proxyChain.length == that.proxyChain.length));
// comparison of actual proxies follows below
equal &=
(this.secure == that.secure) &&
(this.tunnelled == that.tunnelled) &&
(this.layered == that.layered);
// chain length has been compared above, now check the proxies
if (equal && (this.proxyChain != null)) {
for (int i=0; equal && (i<this.proxyChain.length); i++)
equal = this.proxyChain[i].equals(that.proxyChain[i]);
}
return equal;
}
@ -284,8 +438,11 @@ public final class HttpRoute {
if (this.localAddress != null)
hc ^= localAddress.hashCode();
if (this.proxyHost != null)
hc ^= proxyHost.hashCode();
if (this.proxyChain != null) {
hc ^= proxyChain.length;
for (int i=0; i<proxyChain.length; i++)
hc ^= proxyChain[i].hashCode();
}
if (this.secure)
hc ^= 0x11111111;
@ -304,7 +461,7 @@ public final class HttpRoute {
* @return a human-readable representation of this route
*/
public final String toString() {
CharArrayBuffer cab = new CharArrayBuffer(80);
CharArrayBuffer cab = new CharArrayBuffer(50 + getHopCount()*30);
cab.append("HttpRoute[");
if (this.localAddress != null) {
@ -319,9 +476,11 @@ public final class HttpRoute {
if (this.secure)
cab.append('s');
cab.append("}->");
if (this.proxyHost != null) {
cab.append(this.proxyHost);
cab.append("->");
if (this.proxyChain != null) {
for (int i=0; i<this.proxyChain.length; i++) {
cab.append(this.proxyChain[i]);
cab.append("->");
}
}
cab.append(this.targetHost);
cab.append(']');
@ -329,4 +488,11 @@ public final class HttpRoute {
return cab.toString();
}
// default implementation of clone() is sufficient
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
} // class HttpRoute

View File

@ -60,11 +60,14 @@ public class RouteDirector {
/** Step: open connection to proxy. */
public final static int CONNECT_PROXY = 2;
/** Step: tunnel through proxy. */
public final static int CREATE_TUNNEL = 3;
/** Step: tunnel through proxy to target. */
public final static int TUNNEL_TARGET = 3;
/** Step: tunnel through proxy to other proxy. */
public final static int TUNNEL_PROXY = 4;
/** Step: layer protocol (over tunnel). */
public final static int LAYER_PROTOCOL = 4;
public final static int LAYER_PROTOCOL = 5;
// public default constructor
@ -91,10 +94,10 @@ public class RouteDirector {
if (fact == null)
step = firstStep(plan);
else if (plan.getProxyHost() == null)
step = directStep(plan, fact);
else
else if (plan.getHopCount() > 1)
step = proxiedStep(plan, fact);
else
step = directStep(plan, fact);
return step;
@ -110,8 +113,8 @@ public class RouteDirector {
*/
protected int firstStep(HttpRoute plan) {
return (plan.getProxyHost() == null) ?
CONNECT_TARGET : CONNECT_PROXY;
return (plan.getHopCount() > 1) ?
CONNECT_PROXY : CONNECT_TARGET;
}
@ -126,7 +129,7 @@ public class RouteDirector {
*/
protected int directStep(HttpRoute plan, HttpRoute fact) {
if (fact.getProxyHost() != null)
if (fact.getHopCount() > 1)
return UNREACHABLE;
if (!plan.getTargetHost().equals(fact.getTargetHost()))
return UNREACHABLE;
@ -156,19 +159,30 @@ public class RouteDirector {
*/
protected int proxiedStep(HttpRoute plan, HttpRoute fact) {
if (fact.getProxyHost() == null)
if (fact.getHopCount() <= 1)
return UNREACHABLE;
if (!plan.getProxyHost().equals(fact.getProxyHost()) ||
!plan.getTargetHost().equals(fact.getTargetHost()))
if (!plan.getTargetHost().equals(fact.getTargetHost()))
return UNREACHABLE;
final int phc = plan.getHopCount();
final int fhc = fact.getHopCount();
if (phc < fhc)
return UNREACHABLE;
// proxy and target are the same, check tunnelling and layering
for (int i=0; i<phc-1; i++) {
if (!plan.getHopTarget(i).equals(fact.getHopTarget(i)))
return UNREACHABLE;
}
// now we know that the target matches and proxies so far are the same
if (phc > fhc)
return TUNNEL_PROXY; // need to extend the proxy chain
// proxy chain and target are the same, check tunnelling and layering
if ((fact.isTunnelled() && !plan.isTunnelled()) ||
(fact.isLayered() && !plan.isLayered()))
return UNREACHABLE;
if (plan.isTunnelled() && !fact.isTunnelled())
return CREATE_TUNNEL;
return TUNNEL_TARGET;
if (plan.isLayered() && !fact.isLayered())
return LAYER_PROTOCOL;

View File

@ -48,7 +48,7 @@ import org.apache.http.util.CharArrayBuffer;
*
* @since 4.0
*/
public final class RouteTracker {
public final class RouteTracker implements Cloneable {
/** The target host to connect to. */
private final HttpHost targetHost;
@ -65,10 +65,10 @@ public final class RouteTracker {
/** Whether the first hop of the route is established. */
private boolean connected;
/** The proxy server, if any. */
private HttpHost proxyHost;
/** The proxy chain, if any. */
private HttpHost[] proxyChain;
/** Whether the the route is tunnelled through the proxy. */
/** Whether the the route is tunnelled end-to-end through proxies. */
private boolean tunnelled;
/** Whether the route is layered over a tunnel. */
@ -114,13 +114,16 @@ public final class RouteTracker {
* <code>false</code> otherwise
*/
public final void connectTarget(boolean secure) {
if (this.connected) {
throw new IllegalStateException("Already connected.");
}
this.connected = true;
this.secure = secure;
}
/**
* Tracks connecting to a proxy.
* Tracks connecting to the first proxy.
*
* @param proxy the proxy connected to
* @param secure <code>true</code> if the route is secure,
@ -130,20 +133,23 @@ public final class RouteTracker {
if (proxy == null) {
throw new IllegalArgumentException("Proxy host may not be null.");
}
this.connected = true;
this.proxyHost = proxy;
this.secure = secure;
if (this.connected) {
throw new IllegalStateException("Already connected.");
}
this.connected = true;
this.proxyChain = new HttpHost[]{ proxy };
this.secure = secure;
}
/**
* Tracks tunnelling through the proxy.
* Tracks tunnelling to the target.
*
* @param secure <code>true</code> if the route is secure,
* <code>false</code> otherwise
*/
public final void createTunnel(boolean secure) {
if (this.proxyHost == null) {
public final void tunnelTarget(boolean secure) {
if (this.proxyChain == null) {
throw new IllegalStateException("No tunnel without proxy.");
}
if (!this.connected) {
@ -154,6 +160,37 @@ public final class RouteTracker {
}
/**
* Tracks tunnelling to a proxy in a proxy chain.
* This will extend the tracked proxy chain, but it does not mark
* the route as tunnelled. Only end-to-end tunnels are considered there.
*
* @param proxy the proxy tunnelled to
* @param secure <code>true</code> if the route is secure,
* <code>false</code> otherwise
*/
public final void tunnelProxy(HttpHost proxy, boolean secure) {
if (proxy == null) {
throw new IllegalArgumentException("Proxy host may not be null.");
}
if (this.proxyChain == null) {
throw new IllegalStateException("No proxy tunnel without proxy.");
}
if (!this.connected) {
throw new IllegalStateException("No tunnel unless connected.");
}
// prepare an extended proxy chain
HttpHost[] proxies = new HttpHost[this.proxyChain.length+1];
System.arraycopy(this.proxyChain, 0,
proxies, 0, this.proxyChain.length);
proxies[proxies.length+1] = proxy;
this.proxyChain = proxies;
this.secure = secure;
}
/**
* Tracks layering a protocol.
*
@ -194,12 +231,67 @@ public final class RouteTracker {
/**
* Obtains the proxy host.
* Obtains the number of tracked hops.
* An unconnected route has no hops.
* Connecting directly to the target adds one hop.
* Connecting to a proxy adds two hops, one for the proxy and
* one for the target.
* Tunnelling to a proxy in a proxy chain adds one hop.
*
* @return the number of hops in the tracked route
*/
public final int getHopCount() {
int hops = 0;
if (this.connected) {
if (proxyChain == null)
hops = 1;
else
hops = proxyChain.length + 1;
}
return hops;
}
/**
* Obtains the target of a hop in this route.
*
* @param hop index of the hop for which to get the target,
* 0 for first
*
* @return the target of the given hop
*
* @throws IllegalArgumentException
* if the argument is negative or not less than
* {@link #getHopCount getHopCount()}
*/
public final HttpHost getHopTarget(int hop) {
if (hop < 0)
throw new IllegalArgumentException
("Hop index must not be negative: " + hop);
final int hopcount = getHopCount();
if (hop >= hopcount) {
throw new IllegalArgumentException
("Hop index " + hop +
" exceeds tracked route length " + hopcount +".");
}
HttpHost result = null;
if ((this.proxyChain != null) && (hop < this.proxyChain.length))
result = this.proxyChain[hop];
else
result = this.targetHost;
return result;
}
/**
* Obtains the first proxy host.
*
* @return the proxy host, or <code>null</code> if not tracked
* @return the first proxy host, or <code>null</code> if not tracked
*/
public final HttpHost getProxyHost() {
return this.proxyHost;
return (this.proxyChain == null) ? null : this.proxyChain[0];
}
@ -258,7 +350,7 @@ public final class RouteTracker {
public final HttpRoute toRoute() {
return !this.connected ?
null : new HttpRoute(this.targetHost, this.localAddress,
this.proxyHost, this.secure,
this.proxyChain, this.secure,
this.tunnelled, this.layered);
}
@ -277,8 +369,11 @@ public final class RouteTracker {
* @return a representation of the route tracked so far
*/
public final HostConfiguration toHostConfig() {
if ((this.proxyChain != null) && (this.proxyChain.length > 1)) {
throw new IllegalStateException("Cannot convert proxy chain.");
}
return new HostConfiguration
(this.targetHost, this.proxyHost, this.localAddress);
(this.targetHost, getProxyHost(), this.localAddress);
}
@ -299,19 +394,26 @@ public final class RouteTracker {
RouteTracker that = (RouteTracker) o;
boolean equal = this.targetHost.equals(that.targetHost);
equal &=
( this.localAddress == that.localAddress) |
( this.localAddress == that.localAddress) ||
((this.localAddress != null) &&
this.localAddress.equals(that.localAddress));
equal &=
( this.proxyHost == that.proxyHost) |
((this.proxyHost != null) &&
this.proxyHost.equals(that.proxyHost));
( this.proxyChain == that.proxyChain) ||
((this.proxyChain != null) &&
(this.proxyChain.length == that.proxyChain.length));
// comparison of actual proxies follows below
equal &=
(this.connected == that.connected) &&
(this.secure == that.secure) &&
(this.tunnelled == that.tunnelled) &&
(this.layered == that.layered);
// chain length has been compared above, now check the proxies
if (equal && (this.proxyChain != null)) {
for (int i=0; equal && (i<this.proxyChain.length); i++)
equal = this.proxyChain[i].equals(that.proxyChain[i]);
}
return equal;
}
@ -330,8 +432,11 @@ public final class RouteTracker {
if (this.localAddress != null)
hc ^= localAddress.hashCode();
if (this.proxyHost != null)
hc ^= proxyHost.hashCode();
if (this.proxyChain != null) {
hc ^= proxyChain.length;
for (int i=0; i<proxyChain.length; i++)
hc ^= proxyChain[i].hashCode();
}
if (this.connected)
hc ^= 0x11111111;
@ -352,7 +457,7 @@ public final class RouteTracker {
* @return a human-readable representation of the tracked route
*/
public final String toString() {
CharArrayBuffer cab = new CharArrayBuffer(80);
CharArrayBuffer cab = new CharArrayBuffer(50 + getHopCount()*30);
cab.append("RouteTracker[");
if (this.localAddress != null) {
@ -369,9 +474,11 @@ public final class RouteTracker {
if (this.secure)
cab.append('s');
cab.append("}->");
if (this.proxyHost != null) {
cab.append(this.proxyHost);
cab.append("->");
if (this.proxyChain != null) {
for (int i=0; i<this.proxyChain.length; i++) {
cab.append(this.proxyChain[i]);
cab.append("->");
}
}
cab.append(this.targetHost);
cab.append(']');
@ -379,4 +486,11 @@ public final class RouteTracker {
return cab.toString();
}
// default implementation of clone() is sufficient
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
} // class RouteTracker

View File

@ -487,12 +487,16 @@ public class DefaultClientRequestDirector
managedConn.open(route, context, requestExec.getParams());
break;
case RouteDirector.CREATE_TUNNEL:
case RouteDirector.TUNNEL_TARGET:
boolean secure = createTunnel(route, context);
LOG.debug("Tunnel created");
managedConn.tunnelCreated(secure, requestExec.getParams());
break;
case RouteDirector.TUNNEL_PROXY:
throw new UnsupportedOperationException
("Proxy chains are not supported.");
case RouteDirector.LAYER_PROTOCOL:
managedConn.layerProtocol(context, requestExec.getParams());
break;

View File

@ -186,7 +186,7 @@ public abstract class AbstractPoolEntry {
this.connection.update(null, tracker.getTargetHost(),
secure, params);
this.tracker.createTunnel(secure);
this.tracker.tunnelTarget(secure);
} // tunnelCreated