Home | History | Annotate | Download | only in client
      1 /*
      2  * $HeadURL: http://svn.apache.org/repos/asf/httpcomponents/httpclient/trunk/module-client/src/main/java/org/apache/http/impl/client/DefaultRequestDirector.java $
      3  * $Revision: 676023 $
      4  * $Date: 2008-07-11 09:40:56 -0700 (Fri, 11 Jul 2008) $
      5  *
      6  * ====================================================================
      7  * Licensed to the Apache Software Foundation (ASF) under one
      8  * or more contributor license agreements.  See the NOTICE file
      9  * distributed with this work for additional information
     10  * regarding copyright ownership.  The ASF licenses this file
     11  * to you under the Apache License, Version 2.0 (the
     12  * "License"); you may not use this file except in compliance
     13  * with the License.  You may obtain a copy of the License at
     14  *
     15  *   http://www.apache.org/licenses/LICENSE-2.0
     16  *
     17  * Unless required by applicable law or agreed to in writing,
     18  * software distributed under the License is distributed on an
     19  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     20  * KIND, either express or implied.  See the License for the
     21  * specific language governing permissions and limitations
     22  * under the License.
     23  * ====================================================================
     24  *
     25  * This software consists of voluntary contributions made by many
     26  * individuals on behalf of the Apache Software Foundation.  For more
     27  * information on the Apache Software Foundation, please see
     28  * <http://www.apache.org/>.
     29  *
     30  */
     31 
     32 package org.apache.http.impl.client;
     33 
     34 import java.io.IOException;
     35 import java.io.InterruptedIOException;
     36 import java.net.URI;
     37 import java.net.URISyntaxException;
     38 import java.util.Locale;
     39 import java.util.Map;
     40 import java.util.concurrent.TimeUnit;
     41 
     42 import org.apache.commons.logging.Log;
     43 import org.apache.commons.logging.LogFactory;
     44 import org.apache.http.ConnectionReuseStrategy;
     45 import org.apache.http.Header;
     46 import org.apache.http.HttpEntity;
     47 import org.apache.http.HttpEntityEnclosingRequest;
     48 import org.apache.http.HttpException;
     49 import org.apache.http.HttpHost;
     50 import org.apache.http.HttpRequest;
     51 import org.apache.http.HttpResponse;
     52 import org.apache.http.ProtocolException;
     53 import org.apache.http.ProtocolVersion;
     54 import org.apache.http.auth.AuthScheme;
     55 import org.apache.http.auth.AuthScope;
     56 import org.apache.http.auth.AuthState;
     57 import org.apache.http.auth.AuthenticationException;
     58 import org.apache.http.auth.Credentials;
     59 import org.apache.http.auth.MalformedChallengeException;
     60 import org.apache.http.client.AuthenticationHandler;
     61 import org.apache.http.client.RequestDirector;
     62 import org.apache.http.client.CredentialsProvider;
     63 import org.apache.http.client.HttpRequestRetryHandler;
     64 import org.apache.http.client.NonRepeatableRequestException;
     65 import org.apache.http.client.RedirectException;
     66 import org.apache.http.client.RedirectHandler;
     67 import org.apache.http.client.UserTokenHandler;
     68 import org.apache.http.client.methods.AbortableHttpRequest;
     69 import org.apache.http.client.methods.HttpGet;
     70 import org.apache.http.client.methods.HttpUriRequest;
     71 import org.apache.http.client.params.ClientPNames;
     72 import org.apache.http.client.params.HttpClientParams;
     73 import org.apache.http.client.protocol.ClientContext;
     74 import org.apache.http.client.utils.URIUtils;
     75 import org.apache.http.conn.BasicManagedEntity;
     76 import org.apache.http.conn.ClientConnectionManager;
     77 import org.apache.http.conn.ClientConnectionRequest;
     78 import org.apache.http.conn.ConnectionKeepAliveStrategy;
     79 import org.apache.http.conn.ManagedClientConnection;
     80 import org.apache.http.conn.params.ConnManagerParams;
     81 import org.apache.http.conn.routing.BasicRouteDirector;
     82 import org.apache.http.conn.routing.HttpRoute;
     83 import org.apache.http.conn.routing.HttpRouteDirector;
     84 import org.apache.http.conn.routing.HttpRoutePlanner;
     85 import org.apache.http.conn.scheme.Scheme;
     86 import org.apache.http.entity.BufferedHttpEntity;
     87 import org.apache.http.message.BasicHttpRequest;
     88 import org.apache.http.params.HttpConnectionParams;
     89 import org.apache.http.params.HttpParams;
     90 import org.apache.http.params.HttpProtocolParams;
     91 import org.apache.http.protocol.ExecutionContext;
     92 import org.apache.http.protocol.HTTP;
     93 import org.apache.http.protocol.HttpContext;
     94 import org.apache.http.protocol.HttpProcessor;
     95 import org.apache.http.protocol.HttpRequestExecutor;
     96 
     97 /**
     98  * Default implementation of {@link RequestDirector}.
     99  * <br/>
    100  * This class replaces the <code>HttpMethodDirector</code> in HttpClient 3.
    101  *
    102  * @author <a href="mailto:rolandw at apache.org">Roland Weber</a>
    103  * @author <a href="mailto:oleg at ural.ru">Oleg Kalnichevski</a>
    104  *
    105  * <!-- empty lines to avoid svn diff problems -->
    106  * @version $Revision: 676023 $
    107  *
    108  * @since 4.0
    109  */
    110 public class DefaultRequestDirector implements RequestDirector {
    111 
    112     private final Log log = LogFactory.getLog(getClass());
    113 
    114     /** The connection manager. */
    115     protected final ClientConnectionManager connManager;
    116 
    117     /** The route planner. */
    118     protected final HttpRoutePlanner routePlanner;
    119 
    120     /** The connection re-use strategy. */
    121     protected final ConnectionReuseStrategy reuseStrategy;
    122 
    123     /** The keep-alive duration strategy. */
    124     protected final ConnectionKeepAliveStrategy keepAliveStrategy;
    125 
    126     /** The request executor. */
    127     protected final HttpRequestExecutor requestExec;
    128 
    129     /** The HTTP protocol processor. */
    130     protected final HttpProcessor httpProcessor;
    131 
    132     /** The request retry handler. */
    133     protected final HttpRequestRetryHandler retryHandler;
    134 
    135     /** The redirect handler. */
    136     protected final RedirectHandler redirectHandler;
    137 
    138     /** The target authentication handler. */
    139     private final AuthenticationHandler targetAuthHandler;
    140 
    141     /** The proxy authentication handler. */
    142     private final AuthenticationHandler proxyAuthHandler;
    143 
    144     /** The user token handler. */
    145     private final UserTokenHandler userTokenHandler;
    146 
    147     /** The HTTP parameters. */
    148     protected final HttpParams params;
    149 
    150     /** The currently allocated connection. */
    151     protected ManagedClientConnection managedConn;
    152 
    153     private int redirectCount;
    154 
    155     private int maxRedirects;
    156 
    157     private final AuthState targetAuthState;
    158 
    159     private final AuthState proxyAuthState;
    160 
    161     public DefaultRequestDirector(
    162             final HttpRequestExecutor requestExec,
    163             final ClientConnectionManager conman,
    164             final ConnectionReuseStrategy reustrat,
    165             final ConnectionKeepAliveStrategy kastrat,
    166             final HttpRoutePlanner rouplan,
    167             final HttpProcessor httpProcessor,
    168             final HttpRequestRetryHandler retryHandler,
    169             final RedirectHandler redirectHandler,
    170             final AuthenticationHandler targetAuthHandler,
    171             final AuthenticationHandler proxyAuthHandler,
    172             final UserTokenHandler userTokenHandler,
    173             final HttpParams params) {
    174 
    175         if (requestExec == null) {
    176             throw new IllegalArgumentException
    177                 ("Request executor may not be null.");
    178         }
    179         if (conman == null) {
    180             throw new IllegalArgumentException
    181                 ("Client connection manager may not be null.");
    182         }
    183         if (reustrat == null) {
    184             throw new IllegalArgumentException
    185                 ("Connection reuse strategy may not be null.");
    186         }
    187         if (kastrat == null) {
    188             throw new IllegalArgumentException
    189                 ("Connection keep alive strategy may not be null.");
    190         }
    191         if (rouplan == null) {
    192             throw new IllegalArgumentException
    193                 ("Route planner may not be null.");
    194         }
    195         if (httpProcessor == null) {
    196             throw new IllegalArgumentException
    197                 ("HTTP protocol processor may not be null.");
    198         }
    199         if (retryHandler == null) {
    200             throw new IllegalArgumentException
    201                 ("HTTP request retry handler may not be null.");
    202         }
    203         if (redirectHandler == null) {
    204             throw new IllegalArgumentException
    205                 ("Redirect handler may not be null.");
    206         }
    207         if (targetAuthHandler == null) {
    208             throw new IllegalArgumentException
    209                 ("Target authentication handler may not be null.");
    210         }
    211         if (proxyAuthHandler == null) {
    212             throw new IllegalArgumentException
    213                 ("Proxy authentication handler may not be null.");
    214         }
    215         if (userTokenHandler == null) {
    216             throw new IllegalArgumentException
    217                 ("User token handler may not be null.");
    218         }
    219         if (params == null) {
    220             throw new IllegalArgumentException
    221                 ("HTTP parameters may not be null");
    222         }
    223         this.requestExec       = requestExec;
    224         this.connManager       = conman;
    225         this.reuseStrategy     = reustrat;
    226         this.keepAliveStrategy = kastrat;
    227         this.routePlanner      = rouplan;
    228         this.httpProcessor     = httpProcessor;
    229         this.retryHandler      = retryHandler;
    230         this.redirectHandler   = redirectHandler;
    231         this.targetAuthHandler = targetAuthHandler;
    232         this.proxyAuthHandler  = proxyAuthHandler;
    233         this.userTokenHandler  = userTokenHandler;
    234         this.params            = params;
    235 
    236         this.managedConn       = null;
    237 
    238         this.redirectCount = 0;
    239         this.maxRedirects = this.params.getIntParameter(ClientPNames.MAX_REDIRECTS, 100);
    240         this.targetAuthState = new AuthState();
    241         this.proxyAuthState = new AuthState();
    242     } // constructor
    243 
    244 
    245     private RequestWrapper wrapRequest(
    246             final HttpRequest request) throws ProtocolException {
    247         if (request instanceof HttpEntityEnclosingRequest) {
    248             return new EntityEnclosingRequestWrapper(
    249                     (HttpEntityEnclosingRequest) request);
    250         } else {
    251             return new RequestWrapper(
    252                     request);
    253         }
    254     }
    255 
    256 
    257     protected void rewriteRequestURI(
    258             final RequestWrapper request,
    259             final HttpRoute route) throws ProtocolException {
    260         try {
    261 
    262             URI uri = request.getURI();
    263             if (route.getProxyHost() != null && !route.isTunnelled()) {
    264                 // Make sure the request URI is absolute
    265                 if (!uri.isAbsolute()) {
    266                     HttpHost target = route.getTargetHost();
    267                     uri = URIUtils.rewriteURI(uri, target);
    268                     request.setURI(uri);
    269                 }
    270             } else {
    271                 // Make sure the request URI is relative
    272                 if (uri.isAbsolute()) {
    273                     uri = URIUtils.rewriteURI(uri, null);
    274                     request.setURI(uri);
    275                 }
    276             }
    277 
    278         } catch (URISyntaxException ex) {
    279             throw new ProtocolException("Invalid URI: " +
    280                     request.getRequestLine().getUri(), ex);
    281         }
    282     }
    283 
    284 
    285     // non-javadoc, see interface ClientRequestDirector
    286     public HttpResponse execute(HttpHost target, HttpRequest request,
    287                                 HttpContext context)
    288         throws HttpException, IOException {
    289 
    290         HttpRequest orig = request;
    291         RequestWrapper origWrapper = wrapRequest(orig);
    292         origWrapper.setParams(params);
    293         HttpRoute origRoute = determineRoute(target, origWrapper, context);
    294 
    295         RoutedRequest roureq = new RoutedRequest(origWrapper, origRoute);
    296 
    297         long timeout = ConnManagerParams.getTimeout(params);
    298 
    299         int execCount = 0;
    300 
    301         boolean reuse = false;
    302         HttpResponse response = null;
    303         boolean done = false;
    304         try {
    305             while (!done) {
    306                 // In this loop, the RoutedRequest may be replaced by a
    307                 // followup request and route. The request and route passed
    308                 // in the method arguments will be replaced. The original
    309                 // request is still available in 'orig'.
    310 
    311                 RequestWrapper wrapper = roureq.getRequest();
    312                 HttpRoute route = roureq.getRoute();
    313 
    314                 // See if we have a user token bound to the execution context
    315                 Object userToken = context.getAttribute(ClientContext.USER_TOKEN);
    316 
    317                 // Allocate connection if needed
    318                 if (managedConn == null) {
    319                     ClientConnectionRequest connRequest = connManager.requestConnection(
    320                             route, userToken);
    321                     if (orig instanceof AbortableHttpRequest) {
    322                         ((AbortableHttpRequest) orig).setConnectionRequest(connRequest);
    323                     }
    324 
    325                     try {
    326                         managedConn = connRequest.getConnection(timeout, TimeUnit.MILLISECONDS);
    327                     } catch(InterruptedException interrupted) {
    328                         InterruptedIOException iox = new InterruptedIOException();
    329                         iox.initCause(interrupted);
    330                         throw iox;
    331                     }
    332 
    333                     if (HttpConnectionParams.isStaleCheckingEnabled(params)) {
    334                         // validate connection
    335                         this.log.debug("Stale connection check");
    336                         if (managedConn.isStale()) {
    337                             this.log.debug("Stale connection detected");
    338                             // BEGIN android-changed
    339                             try {
    340                                 managedConn.close();
    341                             } catch (IOException ignored) {
    342                                 // SSLSocket's will throw IOException
    343                                 // because they can't send a "close
    344                                 // notify" protocol message to the
    345                                 // server. Just supresss any
    346                                 // exceptions related to closing the
    347                                 // stale connection.
    348                             }
    349                             // END android-changed
    350                         }
    351                     }
    352                 }
    353 
    354                 if (orig instanceof AbortableHttpRequest) {
    355                     ((AbortableHttpRequest) orig).setReleaseTrigger(managedConn);
    356                 }
    357 
    358                 // Reopen connection if needed
    359                 if (!managedConn.isOpen()) {
    360                     managedConn.open(route, context, params);
    361                 }
    362                 // BEGIN android-added
    363                 else {
    364                     // b/3241899 set the per request timeout parameter on reused connections
    365                     managedConn.setSocketTimeout(HttpConnectionParams.getSoTimeout(params));
    366                 }
    367                 // END android-added
    368 
    369                 try {
    370                     establishRoute(route, context);
    371                 } catch (TunnelRefusedException ex) {
    372                     if (this.log.isDebugEnabled()) {
    373                         this.log.debug(ex.getMessage());
    374                     }
    375                     response = ex.getResponse();
    376                     break;
    377                 }
    378 
    379                 // Reset headers on the request wrapper
    380                 wrapper.resetHeaders();
    381 
    382                 // Re-write request URI if needed
    383                 rewriteRequestURI(wrapper, route);
    384 
    385                 // Use virtual host if set
    386                 target = (HttpHost) wrapper.getParams().getParameter(
    387                         ClientPNames.VIRTUAL_HOST);
    388 
    389                 if (target == null) {
    390                     target = route.getTargetHost();
    391                 }
    392 
    393                 HttpHost proxy = route.getProxyHost();
    394 
    395                 // Populate the execution context
    396                 context.setAttribute(ExecutionContext.HTTP_TARGET_HOST,
    397                         target);
    398                 context.setAttribute(ExecutionContext.HTTP_PROXY_HOST,
    399                         proxy);
    400                 context.setAttribute(ExecutionContext.HTTP_CONNECTION,
    401                         managedConn);
    402                 context.setAttribute(ClientContext.TARGET_AUTH_STATE,
    403                         targetAuthState);
    404                 context.setAttribute(ClientContext.PROXY_AUTH_STATE,
    405                         proxyAuthState);
    406 
    407                 // Run request protocol interceptors
    408                 requestExec.preProcess(wrapper, httpProcessor, context);
    409 
    410                 context.setAttribute(ExecutionContext.HTTP_REQUEST,
    411                         wrapper);
    412 
    413                 boolean retrying = true;
    414                 while (retrying) {
    415                     // Increment total exec count (with redirects)
    416                     execCount++;
    417                     // Increment exec count for this particular request
    418                     wrapper.incrementExecCount();
    419                     if (wrapper.getExecCount() > 1 && !wrapper.isRepeatable()) {
    420                         throw new NonRepeatableRequestException("Cannot retry request " +
    421                                 "with a non-repeatable request entity");
    422                     }
    423 
    424                     try {
    425                         if (this.log.isDebugEnabled()) {
    426                             this.log.debug("Attempt " + execCount + " to execute request");
    427                         }
    428                         response = requestExec.execute(wrapper, managedConn, context);
    429                         retrying = false;
    430 
    431                     } catch (IOException ex) {
    432                         this.log.debug("Closing the connection.");
    433                         managedConn.close();
    434                         if (retryHandler.retryRequest(ex, execCount, context)) {
    435                             if (this.log.isInfoEnabled()) {
    436                                 this.log.info("I/O exception ("+ ex.getClass().getName() +
    437                                         ") caught when processing request: "
    438                                         + ex.getMessage());
    439                             }
    440                             if (this.log.isDebugEnabled()) {
    441                                 this.log.debug(ex.getMessage(), ex);
    442                             }
    443                             this.log.info("Retrying request");
    444                         } else {
    445                             throw ex;
    446                         }
    447 
    448                         // If we have a direct route to the target host
    449                         // just re-open connection and re-try the request
    450                         if (route.getHopCount() == 1) {
    451                             this.log.debug("Reopening the direct connection.");
    452                             managedConn.open(route, context, params);
    453                         } else {
    454                             // otherwise give up
    455                             throw ex;
    456                         }
    457 
    458                     }
    459 
    460                 }
    461 
    462                 // Run response protocol interceptors
    463                 response.setParams(params);
    464                 requestExec.postProcess(response, httpProcessor, context);
    465 
    466 
    467                 // The connection is in or can be brought to a re-usable state.
    468                 reuse = reuseStrategy.keepAlive(response, context);
    469                 if(reuse) {
    470                     // Set the idle duration of this connection
    471                     long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
    472                     managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS);
    473                 }
    474 
    475                 RoutedRequest followup = handleResponse(roureq, response, context);
    476                 if (followup == null) {
    477                     done = true;
    478                 } else {
    479                     if (reuse) {
    480                         this.log.debug("Connection kept alive");
    481                         // Make sure the response body is fully consumed, if present
    482                         HttpEntity entity = response.getEntity();
    483                         if (entity != null) {
    484                             entity.consumeContent();
    485                         }
    486                         // entity consumed above is not an auto-release entity,
    487                         // need to mark the connection re-usable explicitly
    488                         managedConn.markReusable();
    489                     } else {
    490                         managedConn.close();
    491                     }
    492                     // check if we can use the same connection for the followup
    493                     if (!followup.getRoute().equals(roureq.getRoute())) {
    494                         releaseConnection();
    495                     }
    496                     roureq = followup;
    497                 }
    498 
    499                 userToken = this.userTokenHandler.getUserToken(context);
    500                 context.setAttribute(ClientContext.USER_TOKEN, userToken);
    501                 if (managedConn != null) {
    502                     managedConn.setState(userToken);
    503                 }
    504             } // while not done
    505 
    506 
    507             // check for entity, release connection if possible
    508             if ((response == null) || (response.getEntity() == null) ||
    509                 !response.getEntity().isStreaming()) {
    510                 // connection not needed and (assumed to be) in re-usable state
    511                 if (reuse)
    512                     managedConn.markReusable();
    513                 releaseConnection();
    514             } else {
    515                 // install an auto-release entity
    516                 HttpEntity entity = response.getEntity();
    517                 entity = new BasicManagedEntity(entity, managedConn, reuse);
    518                 response.setEntity(entity);
    519             }
    520 
    521             return response;
    522 
    523         } catch (HttpException ex) {
    524             abortConnection();
    525             throw ex;
    526         } catch (IOException ex) {
    527             abortConnection();
    528             throw ex;
    529         } catch (RuntimeException ex) {
    530             abortConnection();
    531             throw ex;
    532         }
    533     } // execute
    534 
    535     /**
    536      * Returns the connection back to the connection manager
    537      * and prepares for retrieving a new connection during
    538      * the next request.
    539      */
    540     protected void releaseConnection() {
    541         // Release the connection through the ManagedConnection instead of the
    542         // ConnectionManager directly.  This lets the connection control how
    543         // it is released.
    544         try {
    545             managedConn.releaseConnection();
    546         } catch(IOException ignored) {
    547             this.log.debug("IOException releasing connection", ignored);
    548         }
    549         managedConn = null;
    550     }
    551 
    552     /**
    553      * Determines the route for a request.
    554      * Called by {@link #execute}
    555      * to determine the route for either the original or a followup request.
    556      *
    557      * @param target    the target host for the request.
    558      *                  Implementations may accept <code>null</code>
    559      *                  if they can still determine a route, for example
    560      *                  to a default target or by inspecting the request.
    561      * @param request   the request to execute
    562      * @param context   the context to use for the execution,
    563      *                  never <code>null</code>
    564      *
    565      * @return  the route the request should take
    566      *
    567      * @throws HttpException    in case of a problem
    568      */
    569     protected HttpRoute determineRoute(HttpHost    target,
    570                                            HttpRequest request,
    571                                            HttpContext context)
    572         throws HttpException {
    573 
    574         if (target == null) {
    575             target = (HttpHost) request.getParams().getParameter(
    576                 ClientPNames.DEFAULT_HOST);
    577         }
    578         if (target == null) {
    579             // BEGIN android-changed
    580             //     If the URI was malformed, make it obvious where there's no host component
    581             String scheme = null;
    582             String host = null;
    583             String path = null;
    584             URI uri;
    585             if (request instanceof HttpUriRequest
    586                     && (uri = ((HttpUriRequest) request).getURI()) != null) {
    587                 scheme = uri.getScheme();
    588                 host = uri.getHost();
    589                 path = uri.getPath();
    590             }
    591             throw new IllegalStateException( "Target host must not be null, or set in parameters."
    592                     + " scheme=" + scheme + ", host=" + host + ", path=" + path);
    593             // END android-changed
    594         }
    595 
    596         return this.routePlanner.determineRoute(target, request, context);
    597     }
    598 
    599 
    600     /**
    601      * Establishes the target route.
    602      *
    603      * @param route     the route to establish
    604      * @param context   the context for the request execution
    605      *
    606      * @throws HttpException    in case of a problem
    607      * @throws IOException      in case of an IO problem
    608      */
    609     protected void establishRoute(HttpRoute route, HttpContext context)
    610         throws HttpException, IOException {
    611 
    612         //@@@ how to handle CONNECT requests for tunnelling?
    613         //@@@ refuse to send external CONNECT via director? special handling?
    614 
    615         //@@@ should the request parameters already be used below?
    616         //@@@ probably yes, but they're not linked yet
    617         //@@@ will linking above cause problems with linking in reqExec?
    618         //@@@ probably not, because the parent is replaced
    619         //@@@ just make sure we don't link parameters to themselves
    620 
    621         HttpRouteDirector rowdy = new BasicRouteDirector();
    622         int step;
    623         do {
    624             HttpRoute fact = managedConn.getRoute();
    625             step = rowdy.nextStep(route, fact);
    626 
    627             switch (step) {
    628 
    629             case HttpRouteDirector.CONNECT_TARGET:
    630             case HttpRouteDirector.CONNECT_PROXY:
    631                 managedConn.open(route, context, this.params);
    632                 break;
    633 
    634             case HttpRouteDirector.TUNNEL_TARGET: {
    635                 boolean secure = createTunnelToTarget(route, context);
    636                 this.log.debug("Tunnel to target created.");
    637                 managedConn.tunnelTarget(secure, this.params);
    638             }   break;
    639 
    640             case HttpRouteDirector.TUNNEL_PROXY: {
    641                 // The most simple example for this case is a proxy chain
    642                 // of two proxies, where P1 must be tunnelled to P2.
    643                 // route: Source -> P1 -> P2 -> Target (3 hops)
    644                 // fact:  Source -> P1 -> Target       (2 hops)
    645                 final int hop = fact.getHopCount()-1; // the hop to establish
    646                 boolean secure = createTunnelToProxy(route, hop, context);
    647                 this.log.debug("Tunnel to proxy created.");
    648                 managedConn.tunnelProxy(route.getHopTarget(hop),
    649                                         secure, this.params);
    650             }   break;
    651 
    652 
    653             case HttpRouteDirector.LAYER_PROTOCOL:
    654                 managedConn.layerProtocol(context, this.params);
    655                 break;
    656 
    657             case HttpRouteDirector.UNREACHABLE:
    658                 throw new IllegalStateException
    659                     ("Unable to establish route." +
    660                      "\nplanned = " + route +
    661                      "\ncurrent = " + fact);
    662 
    663             case HttpRouteDirector.COMPLETE:
    664                 // do nothing
    665                 break;
    666 
    667             default:
    668                 throw new IllegalStateException
    669                     ("Unknown step indicator "+step+" from RouteDirector.");
    670             } // switch
    671 
    672         } while (step > HttpRouteDirector.COMPLETE);
    673 
    674     } // establishConnection
    675 
    676 
    677     /**
    678      * Creates a tunnel to the target server.
    679      * The connection must be established to the (last) proxy.
    680      * A CONNECT request for tunnelling through the proxy will
    681      * be created and sent, the response received and checked.
    682      * This method does <i>not</i> update the connection with
    683      * information about the tunnel, that is left to the caller.
    684      *
    685      * @param route     the route to establish
    686      * @param context   the context for request execution
    687      *
    688      * @return  <code>true</code> if the tunnelled route is secure,
    689      *          <code>false</code> otherwise.
    690      *          The implementation here always returns <code>false</code>,
    691      *          but derived classes may override.
    692      *
    693      * @throws HttpException    in case of a problem
    694      * @throws IOException      in case of an IO problem
    695      */
    696     protected boolean createTunnelToTarget(HttpRoute route,
    697                                            HttpContext context)
    698         throws HttpException, IOException {
    699 
    700         HttpHost proxy = route.getProxyHost();
    701         HttpHost target = route.getTargetHost();
    702         HttpResponse response = null;
    703 
    704         boolean done = false;
    705         while (!done) {
    706 
    707             done = true;
    708 
    709             if (!this.managedConn.isOpen()) {
    710                 this.managedConn.open(route, context, this.params);
    711             }
    712 
    713             HttpRequest connect = createConnectRequest(route, context);
    714 
    715             String agent = HttpProtocolParams.getUserAgent(params);
    716             if (agent != null) {
    717                 connect.addHeader(HTTP.USER_AGENT, agent);
    718             }
    719             connect.addHeader(HTTP.TARGET_HOST, target.toHostString());
    720 
    721             AuthScheme authScheme = this.proxyAuthState.getAuthScheme();
    722             AuthScope authScope = this.proxyAuthState.getAuthScope();
    723             Credentials creds = this.proxyAuthState.getCredentials();
    724             if (creds != null) {
    725                 if (authScope != null || !authScheme.isConnectionBased()) {
    726                     try {
    727                         connect.addHeader(authScheme.authenticate(creds, connect));
    728                     } catch (AuthenticationException ex) {
    729                         if (this.log.isErrorEnabled()) {
    730                             this.log.error("Proxy authentication error: " + ex.getMessage());
    731                         }
    732                     }
    733                 }
    734             }
    735 
    736             response = requestExec.execute(connect, this.managedConn, context);
    737 
    738             int status = response.getStatusLine().getStatusCode();
    739             if (status < 200) {
    740                 throw new HttpException("Unexpected response to CONNECT request: " +
    741                         response.getStatusLine());
    742             }
    743 
    744             CredentialsProvider credsProvider = (CredentialsProvider)
    745                 context.getAttribute(ClientContext.CREDS_PROVIDER);
    746 
    747             if (credsProvider != null && HttpClientParams.isAuthenticating(params)) {
    748                 if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) {
    749 
    750                     this.log.debug("Proxy requested authentication");
    751                     Map<String, Header> challenges = this.proxyAuthHandler.getChallenges(
    752                             response, context);
    753                     try {
    754                         processChallenges(
    755                                 challenges, this.proxyAuthState, this.proxyAuthHandler,
    756                                 response, context);
    757                     } catch (AuthenticationException ex) {
    758                         if (this.log.isWarnEnabled()) {
    759                             this.log.warn("Authentication error: " +  ex.getMessage());
    760                             break;
    761                         }
    762                     }
    763                     updateAuthState(this.proxyAuthState, proxy, credsProvider);
    764 
    765                     if (this.proxyAuthState.getCredentials() != null) {
    766                         done = false;
    767 
    768                         // Retry request
    769                         if (this.reuseStrategy.keepAlive(response, context)) {
    770                             this.log.debug("Connection kept alive");
    771                             // Consume response content
    772                             HttpEntity entity = response.getEntity();
    773                             if (entity != null) {
    774                                 entity.consumeContent();
    775                             }
    776                         } else {
    777                             this.managedConn.close();
    778                         }
    779 
    780                     }
    781 
    782                 } else {
    783                     // Reset proxy auth scope
    784                     this.proxyAuthState.setAuthScope(null);
    785                 }
    786             }
    787         }
    788 
    789         int status = response.getStatusLine().getStatusCode();
    790 
    791         if (status > 299) {
    792 
    793             // Buffer response content
    794             HttpEntity entity = response.getEntity();
    795             if (entity != null) {
    796                 response.setEntity(new BufferedHttpEntity(entity));
    797             }
    798 
    799             this.managedConn.close();
    800             throw new TunnelRefusedException("CONNECT refused by proxy: " +
    801                     response.getStatusLine(), response);
    802         }
    803 
    804         this.managedConn.markReusable();
    805 
    806         // How to decide on security of the tunnelled connection?
    807         // The socket factory knows only about the segment to the proxy.
    808         // Even if that is secure, the hop to the target may be insecure.
    809         // Leave it to derived classes, consider insecure by default here.
    810         return false;
    811 
    812     } // createTunnelToTarget
    813 
    814 
    815 
    816     /**
    817      * Creates a tunnel to an intermediate proxy.
    818      * This method is <i>not</i> implemented in this class.
    819      * It just throws an exception here.
    820      *
    821      * @param route     the route to establish
    822      * @param hop       the hop in the route to establish now.
    823      *                  <code>route.getHopTarget(hop)</code>
    824      *                  will return the proxy to tunnel to.
    825      * @param context   the context for request execution
    826      *
    827      * @return  <code>true</code> if the partially tunnelled connection
    828      *          is secure, <code>false</code> otherwise.
    829      *
    830      * @throws HttpException    in case of a problem
    831      * @throws IOException      in case of an IO problem
    832      */
    833     protected boolean createTunnelToProxy(HttpRoute route, int hop,
    834                                           HttpContext context)
    835         throws HttpException, IOException {
    836 
    837         // Have a look at createTunnelToTarget and replicate the parts
    838         // you need in a custom derived class. If your proxies don't require
    839         // authentication, it is not too hard. But for the stock version of
    840         // HttpClient, we cannot make such simplifying assumptions and would
    841         // have to include proxy authentication code. The HttpComponents team
    842         // is currently not in a position to support rarely used code of this
    843         // complexity. Feel free to submit patches that refactor the code in
    844         // createTunnelToTarget to facilitate re-use for proxy tunnelling.
    845 
    846         throw new UnsupportedOperationException
    847             ("Proxy chains are not supported.");
    848     }
    849 
    850 
    851 
    852     /**
    853      * Creates the CONNECT request for tunnelling.
    854      * Called by {@link #createTunnelToTarget createTunnelToTarget}.
    855      *
    856      * @param route     the route to establish
    857      * @param context   the context for request execution
    858      *
    859      * @return  the CONNECT request for tunnelling
    860      */
    861     protected HttpRequest createConnectRequest(HttpRoute route,
    862                                                HttpContext context) {
    863         // see RFC 2817, section 5.2 and
    864         // INTERNET-DRAFT: Tunneling TCP based protocols through
    865         // Web proxy servers
    866 
    867         HttpHost target = route.getTargetHost();
    868 
    869         String host = target.getHostName();
    870         int port = target.getPort();
    871         if (port < 0) {
    872             Scheme scheme = connManager.getSchemeRegistry().
    873                 getScheme(target.getSchemeName());
    874             port = scheme.getDefaultPort();
    875         }
    876 
    877         StringBuilder buffer = new StringBuilder(host.length() + 6);
    878         buffer.append(host);
    879         buffer.append(':');
    880         buffer.append(Integer.toString(port));
    881 
    882         String authority = buffer.toString();
    883         ProtocolVersion ver = HttpProtocolParams.getVersion(params);
    884         HttpRequest req = new BasicHttpRequest
    885             ("CONNECT", authority, ver);
    886 
    887         return req;
    888     }
    889 
    890 
    891     /**
    892      * Analyzes a response to check need for a followup.
    893      *
    894      * @param roureq    the request and route.
    895      * @param response  the response to analayze
    896      * @param context   the context used for the current request execution
    897      *
    898      * @return  the followup request and route if there is a followup, or
    899      *          <code>null</code> if the response should be returned as is
    900      *
    901      * @throws HttpException    in case of a problem
    902      * @throws IOException      in case of an IO problem
    903      */
    904     protected RoutedRequest handleResponse(RoutedRequest roureq,
    905                                            HttpResponse response,
    906                                            HttpContext context)
    907         throws HttpException, IOException {
    908 
    909         HttpRoute route = roureq.getRoute();
    910         HttpHost proxy = route.getProxyHost();
    911         RequestWrapper request = roureq.getRequest();
    912 
    913         HttpParams params = request.getParams();
    914         if (HttpClientParams.isRedirecting(params) &&
    915                 this.redirectHandler.isRedirectRequested(response, context)) {
    916 
    917             if (redirectCount >= maxRedirects) {
    918                 throw new RedirectException("Maximum redirects ("
    919                         + maxRedirects + ") exceeded");
    920             }
    921             redirectCount++;
    922 
    923             URI uri = this.redirectHandler.getLocationURI(response, context);
    924 
    925             HttpHost newTarget = new HttpHost(
    926                     uri.getHost(),
    927                     uri.getPort(),
    928                     uri.getScheme());
    929 
    930             HttpGet redirect = new HttpGet(uri);
    931 
    932             HttpRequest orig = request.getOriginal();
    933             redirect.setHeaders(orig.getAllHeaders());
    934 
    935             RequestWrapper wrapper = new RequestWrapper(redirect);
    936             wrapper.setParams(params);
    937 
    938             HttpRoute newRoute = determineRoute(newTarget, wrapper, context);
    939             RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute);
    940 
    941             if (this.log.isDebugEnabled()) {
    942                 this.log.debug("Redirecting to '" + uri + "' via " + newRoute);
    943             }
    944 
    945             return newRequest;
    946         }
    947 
    948         CredentialsProvider credsProvider = (CredentialsProvider)
    949             context.getAttribute(ClientContext.CREDS_PROVIDER);
    950 
    951         if (credsProvider != null && HttpClientParams.isAuthenticating(params)) {
    952 
    953             if (this.targetAuthHandler.isAuthenticationRequested(response, context)) {
    954 
    955                 HttpHost target = (HttpHost)
    956                     context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
    957                 if (target == null) {
    958                     target = route.getTargetHost();
    959                 }
    960 
    961                 this.log.debug("Target requested authentication");
    962                 Map<String, Header> challenges = this.targetAuthHandler.getChallenges(
    963                         response, context);
    964                 try {
    965                     processChallenges(challenges,
    966                             this.targetAuthState, this.targetAuthHandler,
    967                             response, context);
    968                 } catch (AuthenticationException ex) {
    969                     if (this.log.isWarnEnabled()) {
    970                         this.log.warn("Authentication error: " +  ex.getMessage());
    971                         return null;
    972                     }
    973                 }
    974                 updateAuthState(this.targetAuthState, target, credsProvider);
    975 
    976                 if (this.targetAuthState.getCredentials() != null) {
    977                     // Re-try the same request via the same route
    978                     return roureq;
    979                 } else {
    980                     return null;
    981                 }
    982             } else {
    983                 // Reset target auth scope
    984                 this.targetAuthState.setAuthScope(null);
    985             }
    986 
    987             if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) {
    988 
    989                 this.log.debug("Proxy requested authentication");
    990                 Map<String, Header> challenges = this.proxyAuthHandler.getChallenges(
    991                         response, context);
    992                 try {
    993                     processChallenges(challenges,
    994                             this.proxyAuthState, this.proxyAuthHandler,
    995                             response, context);
    996                 } catch (AuthenticationException ex) {
    997                     if (this.log.isWarnEnabled()) {
    998                         this.log.warn("Authentication error: " +  ex.getMessage());
    999                         return null;
   1000                     }
   1001                 }
   1002                 updateAuthState(this.proxyAuthState, proxy, credsProvider);
   1003 
   1004                 if (this.proxyAuthState.getCredentials() != null) {
   1005                     // Re-try the same request via the same route
   1006                     return roureq;
   1007                 } else {
   1008                     return null;
   1009                 }
   1010             } else {
   1011                 // Reset proxy auth scope
   1012                 this.proxyAuthState.setAuthScope(null);
   1013             }
   1014         }
   1015         return null;
   1016     } // handleResponse
   1017 
   1018 
   1019     /**
   1020      * Shuts down the connection.
   1021      * This method is called from a <code>catch</code> block in
   1022      * {@link #execute execute} during exception handling.
   1023      */
   1024     private void abortConnection() {
   1025         ManagedClientConnection mcc = managedConn;
   1026         if (mcc != null) {
   1027             // we got here as the result of an exception
   1028             // no response will be returned, release the connection
   1029             managedConn = null;
   1030             try {
   1031                 mcc.abortConnection();
   1032             } catch (IOException ex) {
   1033                 if (this.log.isDebugEnabled()) {
   1034                     this.log.debug(ex.getMessage(), ex);
   1035                 }
   1036             }
   1037             // ensure the connection manager properly releases this connection
   1038             try {
   1039                 mcc.releaseConnection();
   1040             } catch(IOException ignored) {
   1041                 this.log.debug("Error releasing connection", ignored);
   1042             }
   1043         }
   1044     } // abortConnection
   1045 
   1046 
   1047     private void processChallenges(
   1048             final Map<String, Header> challenges,
   1049             final AuthState authState,
   1050             final AuthenticationHandler authHandler,
   1051             final HttpResponse response,
   1052             final HttpContext context)
   1053                 throws MalformedChallengeException, AuthenticationException {
   1054 
   1055         AuthScheme authScheme = authState.getAuthScheme();
   1056         if (authScheme == null) {
   1057             // Authentication not attempted before
   1058             authScheme = authHandler.selectScheme(challenges, response, context);
   1059             authState.setAuthScheme(authScheme);
   1060         }
   1061         String id = authScheme.getSchemeName();
   1062 
   1063         Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
   1064         if (challenge == null) {
   1065             throw new AuthenticationException(id +
   1066                 " authorization challenge expected, but not found");
   1067         }
   1068         authScheme.processChallenge(challenge);
   1069         this.log.debug("Authorization challenge processed");
   1070     }
   1071 
   1072 
   1073     private void updateAuthState(
   1074             final AuthState authState,
   1075             final HttpHost host,
   1076             final CredentialsProvider credsProvider) {
   1077 
   1078         if (!authState.isValid()) {
   1079             return;
   1080         }
   1081 
   1082         String hostname = host.getHostName();
   1083         int port = host.getPort();
   1084         if (port < 0) {
   1085             Scheme scheme = connManager.getSchemeRegistry().getScheme(host);
   1086             port = scheme.getDefaultPort();
   1087         }
   1088 
   1089         AuthScheme authScheme = authState.getAuthScheme();
   1090         AuthScope authScope = new AuthScope(
   1091                 hostname,
   1092                 port,
   1093                 authScheme.getRealm(),
   1094                 authScheme.getSchemeName());
   1095 
   1096         if (this.log.isDebugEnabled()) {
   1097             this.log.debug("Authentication scope: " + authScope);
   1098         }
   1099         Credentials creds = authState.getCredentials();
   1100         if (creds == null) {
   1101             creds = credsProvider.getCredentials(authScope);
   1102             if (this.log.isDebugEnabled()) {
   1103                 if (creds != null) {
   1104                     this.log.debug("Found credentials");
   1105                 } else {
   1106                     this.log.debug("Credentials not found");
   1107                 }
   1108             }
   1109         } else {
   1110             if (authScheme.isComplete()) {
   1111                 this.log.debug("Authentication failed");
   1112                 creds = null;
   1113             }
   1114         }
   1115         authState.setAuthScope(authScope);
   1116         authState.setCredentials(creds);
   1117     }
   1118 
   1119 } // class DefaultClientRequestDirector
   1120