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