diff options
Diffstat (limited to 'src/org/apache/http/impl/client/DefaultRequestDirector.java')
-rw-r--r-- | src/org/apache/http/impl/client/DefaultRequestDirector.java | 1088 |
1 files changed, 1088 insertions, 0 deletions
diff --git a/src/org/apache/http/impl/client/DefaultRequestDirector.java b/src/org/apache/http/impl/client/DefaultRequestDirector.java new file mode 100644 index 0000000..511f8a0 --- /dev/null +++ b/src/org/apache/http/impl/client/DefaultRequestDirector.java @@ -0,0 +1,1088 @@ +/* + * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/client/DefaultRequestDirector.java $ + * $Revision: 676023 $ + * $Date: 2008-07-11 09:40:56 -0700 (Fri, 11 Jul 2008) $ + * + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * <http://www.apache.org/>. + * + */ + +package org.apache.http.impl.client; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.http.ConnectionReuseStrategy; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpEntityEnclosingRequest; +import org.apache.http.HttpException; +import org.apache.http.HttpHost; +import org.apache.http.HttpRequest; +import org.apache.http.HttpResponse; +import org.apache.http.ProtocolException; +import org.apache.http.ProtocolVersion; +import org.apache.http.auth.AuthScheme; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.AuthState; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.Credentials; +import org.apache.http.auth.MalformedChallengeException; +import org.apache.http.client.AuthenticationHandler; +import org.apache.http.client.RequestDirector; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.client.NonRepeatableRequestException; +import org.apache.http.client.RedirectException; +import org.apache.http.client.RedirectHandler; +import org.apache.http.client.UserTokenHandler; +import org.apache.http.client.methods.AbortableHttpRequest; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.params.ClientPNames; +import org.apache.http.client.params.HttpClientParams; +import org.apache.http.client.protocol.ClientContext; +import org.apache.http.client.utils.URIUtils; +import org.apache.http.conn.BasicManagedEntity; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.ClientConnectionRequest; +import org.apache.http.conn.ConnectionKeepAliveStrategy; +import org.apache.http.conn.ManagedClientConnection; +import org.apache.http.conn.params.ConnManagerParams; +import org.apache.http.conn.routing.BasicRouteDirector; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.conn.routing.HttpRouteDirector; +import org.apache.http.conn.routing.HttpRoutePlanner; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.entity.BufferedHttpEntity; +import org.apache.http.message.BasicHttpRequest; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.ExecutionContext; +import org.apache.http.protocol.HTTP; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpProcessor; +import org.apache.http.protocol.HttpRequestExecutor; + +/** + * Default implementation of {@link RequestDirector}. + * <br/> + * This class replaces the <code>HttpMethodDirector</code> in HttpClient 3. + * + * @author <a href="mailto:rolandw at apache.org">Roland Weber</a> + * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a> + * + * <!-- empty lines to avoid svn diff problems --> + * @version $Revision: 676023 $ + * + * @since 4.0 + */ +public class DefaultRequestDirector implements RequestDirector { + + private final Log log = LogFactory.getLog(getClass()); + + /** The connection manager. */ + protected final ClientConnectionManager connManager; + + /** The route planner. */ + protected final HttpRoutePlanner routePlanner; + + /** The connection re-use strategy. */ + protected final ConnectionReuseStrategy reuseStrategy; + + /** The keep-alive duration strategy. */ + protected final ConnectionKeepAliveStrategy keepAliveStrategy; + + /** The request executor. */ + protected final HttpRequestExecutor requestExec; + + /** The HTTP protocol processor. */ + protected final HttpProcessor httpProcessor; + + /** The request retry handler. */ + protected final HttpRequestRetryHandler retryHandler; + + /** The redirect handler. */ + protected final RedirectHandler redirectHandler; + + /** The target authentication handler. */ + private final AuthenticationHandler targetAuthHandler; + + /** The proxy authentication handler. */ + private final AuthenticationHandler proxyAuthHandler; + + /** The user token handler. */ + private final UserTokenHandler userTokenHandler; + + /** The HTTP parameters. */ + protected final HttpParams params; + + /** The currently allocated connection. */ + protected ManagedClientConnection managedConn; + + private int redirectCount; + + private int maxRedirects; + + private final AuthState targetAuthState; + + private final AuthState proxyAuthState; + + public DefaultRequestDirector( + final HttpRequestExecutor requestExec, + final ClientConnectionManager conman, + final ConnectionReuseStrategy reustrat, + final ConnectionKeepAliveStrategy kastrat, + final HttpRoutePlanner rouplan, + final HttpProcessor httpProcessor, + final HttpRequestRetryHandler retryHandler, + final RedirectHandler redirectHandler, + final AuthenticationHandler targetAuthHandler, + final AuthenticationHandler proxyAuthHandler, + final UserTokenHandler userTokenHandler, + final HttpParams params) { + + if (requestExec == null) { + throw new IllegalArgumentException + ("Request executor may not be null."); + } + if (conman == null) { + throw new IllegalArgumentException + ("Client connection manager may not be null."); + } + if (reustrat == null) { + throw new IllegalArgumentException + ("Connection reuse strategy may not be null."); + } + if (kastrat == null) { + throw new IllegalArgumentException + ("Connection keep alive strategy may not be null."); + } + if (rouplan == null) { + throw new IllegalArgumentException + ("Route planner may not be null."); + } + if (httpProcessor == null) { + throw new IllegalArgumentException + ("HTTP protocol processor may not be null."); + } + if (retryHandler == null) { + throw new IllegalArgumentException + ("HTTP request retry handler may not be null."); + } + if (redirectHandler == null) { + throw new IllegalArgumentException + ("Redirect handler may not be null."); + } + if (targetAuthHandler == null) { + throw new IllegalArgumentException + ("Target authentication handler may not be null."); + } + if (proxyAuthHandler == null) { + throw new IllegalArgumentException + ("Proxy authentication handler may not be null."); + } + if (userTokenHandler == null) { + throw new IllegalArgumentException + ("User token handler may not be null."); + } + if (params == null) { + throw new IllegalArgumentException + ("HTTP parameters may not be null"); + } + this.requestExec = requestExec; + this.connManager = conman; + this.reuseStrategy = reustrat; + this.keepAliveStrategy = kastrat; + this.routePlanner = rouplan; + this.httpProcessor = httpProcessor; + this.retryHandler = retryHandler; + this.redirectHandler = redirectHandler; + this.targetAuthHandler = targetAuthHandler; + this.proxyAuthHandler = proxyAuthHandler; + this.userTokenHandler = userTokenHandler; + this.params = params; + + this.managedConn = null; + + this.redirectCount = 0; + this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100); + this.targetAuthState = new AuthState(); + this.proxyAuthState = new AuthState(); + } // constructor + + + private RequestWrapper wrapRequest( + final HttpRequest request) throws ProtocolException { + if (request instanceof HttpEntityEnclosingRequest) { + return new EntityEnclosingRequestWrapper( + (HttpEntityEnclosingRequest) request); + } else { + return new RequestWrapper( + request); + } + } + + + protected void rewriteRequestURI( + final RequestWrapper request, + final HttpRoute route) throws ProtocolException { + try { + + URI uri = request.getURI(); + if (route.getProxyHost() != null && !route.isTunnelled()) { + // Make sure the request URI is absolute + if (!uri.isAbsolute()) { + HttpHost target = route.getTargetHost(); + uri = URIUtils.rewriteURI(uri, target); + request.setURI(uri); + } + } else { + // Make sure the request URI is relative + if (uri.isAbsolute()) { + uri = URIUtils.rewriteURI(uri, null); + request.setURI(uri); + } + } + + } catch (URISyntaxException ex) { + throw new ProtocolException("Invalid URI: " + + request.getRequestLine().getUri(), ex); + } + } + + + // non-javadoc, see interface ClientRequestDirector + public HttpResponse execute(HttpHost target, HttpRequest request, + HttpContext context) + throws HttpException, IOException { + + HttpRequest orig = request; + RequestWrapper origWrapper = wrapRequest(orig); + origWrapper.setParams(params); + HttpRoute origRoute = determineRoute(target, origWrapper, context); + + RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute); + + long timeout = ConnManagerParams.getTimeout(params); + + int execCount = 0; + + boolean reuse = false; + HttpResponse response = null; + boolean done = false; + try { + while (!done) { + // In this loop, the RoutedRequest may be replaced by a + // followup request and route. The request and route passed + // in the method arguments will be replaced. The original + // request is still available in 'orig'. + + RequestWrapper wrapper = roureq.getRequest(); + HttpRoute route = roureq.getRoute(); + + // See if we have a user token bound to the execution context + Object userToken = context.getAttribute(ClientContext.USER_TOKEN); + + // Allocate connection if needed + if (managedConn == null) { + ClientConnectionRequest connRequest = connManager.requestConnection( + route, userToken); + if (orig instanceof AbortableHttpRequest) { + ((AbortableHttpRequest) orig).setConnectionRequest(connRequest); + } + + try { + managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS); + } catch(InterruptedException interrupted) { + InterruptedIOException iox = new InterruptedIOException(); + iox.initCause(interrupted); + throw iox; + } + + if (HttpConnectionParams.isStaleCheckingEnabled(params)) { + // validate connection + this.log.debug("Stale connection check"); + if (managedConn.isStale()) { + this.log.debug("Stale connection detected"); + managedConn.close(); + } + } + } + + if (orig instanceof AbortableHttpRequest) { + ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn); + } + + // Reopen connection if needed + if (!managedConn.isOpen()) { + managedConn.open(route, context, params); + } + + try { + establishRoute(route, context); + } catch (TunnelRefusedException ex) { + if (this.log.isDebugEnabled()) { + this.log.debug(ex.getMessage()); + } + response = ex.getResponse(); + break; + } + + // Reset headers on the request wrapper + wrapper.resetHeaders(); + + // Re-write request URI if needed + rewriteRequestURI(wrapper, route); + + // Use virtual host if set + target = (HttpHost) wrapper.getParams().getParameter( + ClientPNames.VIRTUAL_HOST); + + if (target == null) { + target = route.getTargetHost(); + } + + HttpHost proxy = route.getProxyHost(); + + // Populate the execution context + context.setAttribute(ExecutionContext.HTTP_TARGET_HOST, + target); + context.setAttribute(ExecutionContext.HTTP_PROXY_HOST, + proxy); + context.setAttribute(ExecutionContext.HTTP_CONNECTION, + managedConn); + context.setAttribute(ClientContext.TARGET_AUTH_STATE, + targetAuthState); + context.setAttribute(ClientContext.PROXY_AUTH_STATE, + proxyAuthState); + + // Run request protocol interceptors + requestExec.preProcess(wrapper, httpProcessor, context); + + context.setAttribute(ExecutionContext.HTTP_REQUEST, + wrapper); + + boolean retrying = true; + while (retrying) { + // Increment total exec count (with redirects) + execCount++; + // Increment exec count for this particular request + wrapper.incrementExecCount(); + if (wrapper.getExecCount() > 1 && !wrapper.isRepeatable()) { + throw new NonRepeatableRequestException("Cannot retry request " + + "with a non-repeatable request entity"); + } + + try { + if (this.log.isDebugEnabled()) { + this.log.debug("Attempt " + execCount + " to execute request"); + } + response = requestExec.execute(wrapper, managedConn, context); + retrying = false; + + } catch (IOException ex) { + this.log.debug("Closing the connection."); + managedConn.close(); + if (retryHandler.retryRequest(ex, execCount, context)) { + if (this.log.isInfoEnabled()) { + this.log.info("I/O exception ("+ ex.getClass().getName() + + ") caught when processing request: " + + ex.getMessage()); + } + if (this.log.isDebugEnabled()) { + this.log.debug(ex.getMessage(), ex); + } + this.log.info("Retrying request"); + } else { + throw ex; + } + + // If we have a direct route to the target host + // just re-open connection and re-try the request + if (route.getHopCount() == 1) { + this.log.debug("Reopening the direct connection."); + managedConn.open(route, context, params); + } else { + // otherwise give up + retrying = false; + } + + } + + } + + // Run response protocol interceptors + response.setParams(params); + requestExec.postProcess(response, httpProcessor, context); + + + // The connection is in or can be brought to a re-usable state. + reuse = reuseStrategy.keepAlive(response, context); + if(reuse) { + // Set the idle duration of this connection + long duration = keepAliveStrategy.getKeepAliveDuration(response, context); + managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS); + } + + RoutedRequest followup = handleResponse(roureq, response, context); + if (followup == null) { + done = true; + } else { + if (reuse) { + this.log.debug("Connection kept alive"); + // Make sure the response body is fully consumed, if present + HttpEntity entity = response.getEntity(); + if (entity != null) { + entity.consumeContent(); + } + // entity consumed above is not an auto-release entity, + // need to mark the connection re-usable explicitly + managedConn.markReusable(); + } else { + managedConn.close(); + } + // check if we can use the same connection for the followup + if (!followup.getRoute().equals(roureq.getRoute())) { + releaseConnection(); + } + roureq = followup; + } + + userToken = this.userTokenHandler.getUserToken(context); + context.setAttribute(ClientContext.USER_TOKEN, userToken); + if (managedConn != null) { + managedConn.setState(userToken); + } + } // while not done + + + // check for entity, release connection if possible + if ((response == null) || (response.getEntity() == null) || + !response.getEntity().isStreaming()) { + // connection not needed and (assumed to be) in re-usable state + if (reuse) + managedConn.markReusable(); + releaseConnection(); + } else { + // install an auto-release entity + HttpEntity entity = response.getEntity(); + entity = new BasicManagedEntity(entity, managedConn, reuse); + response.setEntity(entity); + } + + return response; + + } catch (HttpException ex) { + abortConnection(); + throw ex; + } catch (IOException ex) { + abortConnection(); + throw ex; + } catch (RuntimeException ex) { + abortConnection(); + throw ex; + } + } // execute + + /** + * Returns the connection back to the connection manager + * and prepares for retrieving a new connection during + * the next request. + */ + protected void releaseConnection() { + // Release the connection through the ManagedConnection instead of the + // ConnectionManager directly. This lets the connection control how + // it is released. + try { + managedConn.releaseConnection(); + } catch(IOException ignored) { + this.log.debug("IOException releasing connection", ignored); + } + managedConn = null; + } + + /** + * Determines the route for a request. + * Called by {@link #execute} + * to determine the route for either the original or a followup request. + * + * @param target the target host for the request. + * Implementations may accept <code>null</code> + * if they can still determine a route, for example + * to a default target or by inspecting the request. + * @param request the request to execute + * @param context the context to use for the execution, + * never <code>null</code> + * + * @return the route the request should take + * + * @throws HttpException in case of a problem + */ + protected HttpRoute determineRoute(HttpHost target, + HttpRequest request, + HttpContext context) + throws HttpException { + + if (target == null) { + target = (HttpHost) request.getParams().getParameter( + ClientPNames.DEFAULT_HOST); + } + if (target == null) { + throw new IllegalStateException + ("Target host must not be null, or set in parameters."); + } + + return this.routePlanner.determineRoute(target, request, context); + } + + + /** + * Establishes the target route. + * + * @param route the route to establish + * @param context the context for the request execution + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected void establishRoute(HttpRoute route, HttpContext context) + throws HttpException, IOException { + + //@@@ how to handle CONNECT requests for tunnelling? + //@@@ refuse to send external CONNECT via director? special handling? + + //@@@ should the request parameters already be used below? + //@@@ probably yes, but they're not linked yet + //@@@ will linking above cause problems with linking in reqExec? + //@@@ probably not, because the parent is replaced + //@@@ just make sure we don't link parameters to themselves + + HttpRouteDirector rowdy = new BasicRouteDirector(); + int step; + do { + HttpRoute fact = managedConn.getRoute(); + step = rowdy.nextStep(route, fact); + + switch (step) { + + case HttpRouteDirector.CONNECT_TARGET: + case HttpRouteDirector.CONNECT_PROXY: + managedConn.open(route, context, this.params); + break; + + case HttpRouteDirector.TUNNEL_TARGET: { + boolean secure = createTunnelToTarget(route, context); + this.log.debug("Tunnel to target created."); + managedConn.tunnelTarget(secure, this.params); + } break; + + case HttpRouteDirector.TUNNEL_PROXY: { + // The most simple example for this case is a proxy chain + // of two proxies, where P1 must be tunnelled to P2. + // route: Source -> P1 -> P2 -> Target (3 hops) + // fact: Source -> P1 -> Target (2 hops) + final int hop = fact.getHopCount()-1; // the hop to establish + boolean secure = createTunnelToProxy(route, hop, context); + this.log.debug("Tunnel to proxy created."); + managedConn.tunnelProxy(route.getHopTarget(hop), + secure, this.params); + } break; + + + case HttpRouteDirector.LAYER_PROTOCOL: + managedConn.layerProtocol(context, this.params); + break; + + case HttpRouteDirector.UNREACHABLE: + throw new IllegalStateException + ("Unable to establish route." + + "\nplanned = " + route + + "\ncurrent = " + fact); + + case HttpRouteDirector.COMPLETE: + // do nothing + break; + + default: + throw new IllegalStateException + ("Unknown step indicator "+step+" from RouteDirector."); + } // switch + + } while (step > HttpRouteDirector.COMPLETE); + + } // establishConnection + + + /** + * Creates a tunnel to the target server. + * The connection must be established to the (last) proxy. + * A CONNECT request for tunnelling through the proxy will + * be created and sent, the response received and checked. + * This method does <i>not</i> update the connection with + * information about the tunnel, that is left to the caller. + * + * @param route the route to establish + * @param context the context for request execution + * + * @return <code>true</code> if the tunnelled route is secure, + * <code>false</code> otherwise. + * The implementation here always returns <code>false</code>, + * but derived classes may override. + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected boolean createTunnelToTarget(HttpRoute route, + HttpContext context) + throws HttpException, IOException { + + HttpHost proxy = route.getProxyHost(); + HttpHost target = route.getTargetHost(); + HttpResponse response = null; + + boolean done = false; + while (!done) { + + done = true; + + if (!this.managedConn.isOpen()) { + this.managedConn.open(route, context, this.params); + } + + HttpRequest connect = createConnectRequest(route, context); + + String agent = HttpProtocolParams.getUserAgent(params); + if (agent != null) { + connect.addHeader(HTTP.USER_AGENT, agent); + } + connect.addHeader(HTTP.TARGET_HOST, target.toHostString()); + + AuthScheme authScheme = this.proxyAuthState.getAuthScheme(); + AuthScope authScope = this.proxyAuthState.getAuthScope(); + Credentials creds = this.proxyAuthState.getCredentials(); + if (creds != null) { + if (authScope != null || !authScheme.isConnectionBased()) { + try { + connect.addHeader(authScheme.authenticate(creds, connect)); + } catch (AuthenticationException ex) { + if (this.log.isErrorEnabled()) { + this.log.error("Proxy authentication error: " + ex.getMessage()); + } + } + } + } + + response = requestExec.execute(connect, this.managedConn, context); + + int status = response.getStatusLine().getStatusCode(); + if (status < 200) { + throw new HttpException("Unexpected response to CONNECT request: " + + response.getStatusLine()); + } + + CredentialsProvider credsProvider = (CredentialsProvider) + context.getAttribute(ClientContext.CREDS_PROVIDER); + + if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { + if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { + + this.log.debug("Proxy requested authentication"); + Map<String, Header> challenges = this.proxyAuthHandler.getChallenges( + response, context); + try { + processChallenges( + challenges, this.proxyAuthState, this.proxyAuthHandler, + response, context); + } catch (AuthenticationException ex) { + if (this.log.isWarnEnabled()) { + this.log.warn("Authentication error: " + ex.getMessage()); + break; + } + } + updateAuthState(this.proxyAuthState, proxy, credsProvider); + + if (this.proxyAuthState.getCredentials() != null) { + done = false; + + // Retry request + if (this.reuseStrategy.keepAlive(response, context)) { + this.log.debug("Connection kept alive"); + // Consume response content + HttpEntity entity = response.getEntity(); + if (entity != null) { + entity.consumeContent(); + } + } else { + this.managedConn.close(); + } + + } + + } else { + // Reset proxy auth scope + this.proxyAuthState.setAuthScope(null); + } + } + } + + int status = response.getStatusLine().getStatusCode(); + + if (status > 299) { + + // Buffer response content + HttpEntity entity = response.getEntity(); + if (entity != null) { + response.setEntity(new BufferedHttpEntity(entity)); + } + + this.managedConn.close(); + throw new TunnelRefusedException("CONNECT refused by proxy: " + + response.getStatusLine(), response); + } + + this.managedConn.markReusable(); + + // How to decide on security of the tunnelled connection? + // The socket factory knows only about the segment to the proxy. + // Even if that is secure, the hop to the target may be insecure. + // Leave it to derived classes, consider insecure by default here. + return false; + + } // createTunnelToTarget + + + + /** + * Creates a tunnel to an intermediate proxy. + * This method is <i>not</i> implemented in this class. + * It just throws an exception here. + * + * @param route the route to establish + * @param hop the hop in the route to establish now. + * <code>route.getHopTarget(hop)</code> + * will return the proxy to tunnel to. + * @param context the context for request execution + * + * @return <code>true</code> if the partially tunnelled connection + * is secure, <code>false</code> otherwise. + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected boolean createTunnelToProxy(HttpRoute route, int hop, + HttpContext context) + throws HttpException, IOException { + + // Have a look at createTunnelToTarget and replicate the parts + // you need in a custom derived class. If your proxies don't require + // authentication, it is not too hard. But for the stock version of + // HttpClient, we cannot make such simplifying assumptions and would + // have to include proxy authentication code. The HttpComponents team + // is currently not in a position to support rarely used code of this + // complexity. Feel free to submit patches that refactor the code in + // createTunnelToTarget to facilitate re-use for proxy tunnelling. + + throw new UnsupportedOperationException + ("Proxy chains are not supported."); + } + + + + /** + * Creates the CONNECT request for tunnelling. + * Called by {@link #createTunnelToTarget createTunnelToTarget}. + * + * @param route the route to establish + * @param context the context for request execution + * + * @return the CONNECT request for tunnelling + */ + protected HttpRequest createConnectRequest(HttpRoute route, + HttpContext context) { + // see RFC 2817, section 5.2 and + // INTERNET-DRAFT: Tunneling TCP based protocols through + // Web proxy servers + + HttpHost target = route.getTargetHost(); + + String host = target.getHostName(); + int port = target.getPort(); + if (port < 0) { + Scheme scheme = connManager.getSchemeRegistry(). + getScheme(target.getSchemeName()); + port = scheme.getDefaultPort(); + } + + StringBuilder buffer = new StringBuilder(host.length() + 6); + buffer.append(host); + buffer.append(':'); + buffer.append(Integer.toString(port)); + + String authority = buffer.toString(); + ProtocolVersion ver = HttpProtocolParams.getVersion(params); + HttpRequest req = new BasicHttpRequest + ("CONNECT", authority, ver); + + return req; + } + + + /** + * Analyzes a response to check need for a followup. + * + * @param roureq the request and route. + * @param response the response to analayze + * @param context the context used for the current request execution + * + * @return the followup request and route if there is a followup, or + * <code>null</code> if the response should be returned as is + * + * @throws HttpException in case of a problem + * @throws IOException in case of an IO problem + */ + protected RoutedRequest handleResponse(RoutedRequest roureq, + HttpResponse response, + HttpContext context) + throws HttpException, IOException { + + HttpRoute route = roureq.getRoute(); + HttpHost proxy = route.getProxyHost(); + RequestWrapper request = roureq.getRequest(); + + HttpParams params = request.getParams(); + if (HttpClientParams.isRedirecting(params) && + this.redirectHandler.isRedirectRequested(response, context)) { + + if (redirectCount >= maxRedirects) { + throw new RedirectException("Maximum redirects (" + + maxRedirects + ") exceeded"); + } + redirectCount++; + + URI uri = this.redirectHandler.getLocationURI(response, context); + + HttpHost newTarget = new HttpHost( + uri.getHost(), + uri.getPort(), + uri.getScheme()); + + HttpGet redirect = new HttpGet(uri); + + HttpRequest orig = request.getOriginal(); + redirect.setHeaders(orig.getAllHeaders()); + + RequestWrapper wrapper = new RequestWrapper(redirect); + wrapper.setParams(params); + + HttpRoute newRoute = determineRoute(newTarget, wrapper, context); + RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute); + + if (this.log.isDebugEnabled()) { + this.log.debug("Redirecting to '" + uri + "' via " + newRoute); + } + + return newRequest; + } + + CredentialsProvider credsProvider = (CredentialsProvider) + context.getAttribute(ClientContext.CREDS_PROVIDER); + + if (credsProvider != null && HttpClientParams.isAuthenticating(params)) { + + if (this.targetAuthHandler.isAuthenticationRequested(response, context)) { + + HttpHost target = (HttpHost) + context.getAttribute(ExecutionContext.HTTP_TARGET_HOST); + if (target == null) { + target = route.getTargetHost(); + } + + this.log.debug("Target requested authentication"); + Map<String, Header> challenges = this.targetAuthHandler.getChallenges( + response, context); + try { + processChallenges(challenges, + this.targetAuthState, this.targetAuthHandler, + response, context); + } catch (AuthenticationException ex) { + if (this.log.isWarnEnabled()) { + this.log.warn("Authentication error: " + ex.getMessage()); + return null; + } + } + updateAuthState(this.targetAuthState, target, credsProvider); + + if (this.targetAuthState.getCredentials() != null) { + // Re-try the same request via the same route + return roureq; + } else { + return null; + } + } else { + // Reset target auth scope + this.targetAuthState.setAuthScope(null); + } + + if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) { + + this.log.debug("Proxy requested authentication"); + Map<String, Header> challenges = this.proxyAuthHandler.getChallenges( + response, context); + try { + processChallenges(challenges, + this.proxyAuthState, this.proxyAuthHandler, + response, context); + } catch (AuthenticationException ex) { + if (this.log.isWarnEnabled()) { + this.log.warn("Authentication error: " + ex.getMessage()); + return null; + } + } + updateAuthState(this.proxyAuthState, proxy, credsProvider); + + if (this.proxyAuthState.getCredentials() != null) { + // Re-try the same request via the same route + return roureq; + } else { + return null; + } + } else { + // Reset proxy auth scope + this.proxyAuthState.setAuthScope(null); + } + } + return null; + } // handleResponse + + + /** + * Shuts down the connection. + * This method is called from a <code>catch</code> block in + * {@link #execute execute} during exception handling. + */ + private void abortConnection() { + ManagedClientConnection mcc = managedConn; + if (mcc != null) { + // we got here as the result of an exception + // no response will be returned, release the connection + managedConn = null; + try { + mcc.abortConnection(); + } catch (IOException ex) { + if (this.log.isDebugEnabled()) { + this.log.debug(ex.getMessage(), ex); + } + } + // ensure the connection manager properly releases this connection + try { + mcc.releaseConnection(); + } catch(IOException ignored) { + this.log.debug("Error releasing connection", ignored); + } + } + } // abortConnection + + + private void processChallenges( + final Map<String, Header> challenges, + final AuthState authState, + final AuthenticationHandler authHandler, + final HttpResponse response, + final HttpContext context) + throws MalformedChallengeException, AuthenticationException { + + AuthScheme authScheme = authState.getAuthScheme(); + if (authScheme == null) { + // Authentication not attempted before + authScheme = authHandler.selectScheme(challenges, response, context); + authState.setAuthScheme(authScheme); + } + String id = authScheme.getSchemeName(); + + Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH)); + if (challenge == null) { + throw new AuthenticationException(id + + " authorization challenge expected, but not found"); + } + authScheme.processChallenge(challenge); + this.log.debug("Authorization challenge processed"); + } + + + private void updateAuthState( + final AuthState authState, + final HttpHost host, + final CredentialsProvider credsProvider) { + + if (!authState.isValid()) { + return; + } + + String hostname = host.getHostName(); + int port = host.getPort(); + if (port < 0) { + Scheme scheme = connManager.getSchemeRegistry().getScheme(host); + port = scheme.getDefaultPort(); + } + + AuthScheme authScheme = authState.getAuthScheme(); + AuthScope authScope = new AuthScope( + hostname, + port, + authScheme.getRealm(), + authScheme.getSchemeName()); + + if (this.log.isDebugEnabled()) { + this.log.debug("Authentication scope: " + authScope); + } + Credentials creds = authState.getCredentials(); + if (creds == null) { + creds = credsProvider.getCredentials(authScope); + if (this.log.isDebugEnabled()) { + if (creds != null) { + this.log.debug("Found credentials"); + } else { + this.log.debug("Credentials not found"); + } + } + } else { + if (authScheme.isComplete()) { + this.log.debug("Authentication failed"); + creds = null; + } + } + authState.setAuthScope(authScope); + authState.setCredentials(creds); + } + +} // class DefaultClientRequestDirector |