DefaultRequestDirector.java revision 2d8fd9bea30efcae4cc418e223f2c61febe0caa0
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
32package org.apache.http.impl.client;
33
34import java.io.IOException;
35import java.io.InterruptedIOException;
36import java.net.URI;
37import java.net.URISyntaxException;
38import java.util.Locale;
39import java.util.Map;
40import java.util.concurrent.TimeUnit;
41
42import org.apache.commons.logging.Log;
43import org.apache.commons.logging.LogFactory;
44import org.apache.http.ConnectionReuseStrategy;
45import org.apache.http.Header;
46import org.apache.http.HttpEntity;
47import org.apache.http.HttpEntityEnclosingRequest;
48import org.apache.http.HttpException;
49import org.apache.http.HttpHost;
50import org.apache.http.HttpRequest;
51import org.apache.http.HttpResponse;
52import org.apache.http.ProtocolException;
53import org.apache.http.ProtocolVersion;
54import org.apache.http.auth.AuthScheme;
55import org.apache.http.auth.AuthScope;
56import org.apache.http.auth.AuthState;
57import org.apache.http.auth.AuthenticationException;
58import org.apache.http.auth.Credentials;
59import org.apache.http.auth.MalformedChallengeException;
60import org.apache.http.client.AuthenticationHandler;
61import org.apache.http.client.RequestDirector;
62import org.apache.http.client.CredentialsProvider;
63import org.apache.http.client.HttpRequestRetryHandler;
64import org.apache.http.client.NonRepeatableRequestException;
65import org.apache.http.client.RedirectException;
66import org.apache.http.client.RedirectHandler;
67import org.apache.http.client.UserTokenHandler;
68import org.apache.http.client.methods.AbortableHttpRequest;
69import org.apache.http.client.methods.HttpGet;
70import org.apache.http.client.methods.HttpUriRequest;
71import org.apache.http.client.params.ClientPNames;
72import org.apache.http.client.params.HttpClientParams;
73import org.apache.http.client.protocol.ClientContext;
74import org.apache.http.client.utils.URIUtils;
75import org.apache.http.conn.BasicManagedEntity;
76import org.apache.http.conn.ClientConnectionManager;
77import org.apache.http.conn.ClientConnectionRequest;
78import org.apache.http.conn.ConnectionKeepAliveStrategy;
79import org.apache.http.conn.ManagedClientConnection;
80import org.apache.http.conn.params.ConnManagerParams;
81import org.apache.http.conn.routing.BasicRouteDirector;
82import org.apache.http.conn.routing.HttpRoute;
83import org.apache.http.conn.routing.HttpRouteDirector;
84import org.apache.http.conn.routing.HttpRoutePlanner;
85import org.apache.http.conn.scheme.Scheme;
86import org.apache.http.entity.BufferedHttpEntity;
87import org.apache.http.message.BasicHttpRequest;
88import org.apache.http.params.HttpConnectionParams;
89import org.apache.http.params.HttpParams;
90import org.apache.http.params.HttpProtocolParams;
91import org.apache.http.protocol.ExecutionContext;
92import org.apache.http.protocol.HTTP;
93import org.apache.http.protocol.HttpContext;
94import org.apache.http.protocol.HttpProcessor;
95import 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 */
110public 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                            retrying = false;
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