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