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