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