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()) && (!isCleartextTrafficPermitted())) {
436                            throw new IOException(
437                                    "Cleartext traffic not permitted: " + route.getTargetHost());
438                        }
439                        // END android-added
440                        response = requestExec.execute(wrapper, managedConn, context);
441                        retrying = false;
442
443                    } catch (IOException ex) {
444                        this.log.debug("Closing the connection.");
445                        managedConn.close();
446                        if (retryHandler.retryRequest(ex, execCount, context)) {
447                            if (this.log.isInfoEnabled()) {
448                                this.log.info("I/O exception ("+ ex.getClass().getName() +
449                                        ") caught when processing request: "
450                                        + ex.getMessage());
451                            }
452                            if (this.log.isDebugEnabled()) {
453                                this.log.debug(ex.getMessage(), ex);
454                            }
455                            this.log.info("Retrying request");
456                        } else {
457                            throw ex;
458                        }
459
460                        // If we have a direct route to the target host
461                        // just re-open connection and re-try the request
462                        if (route.getHopCount() == 1) {
463                            this.log.debug("Reopening the direct connection.");
464                            managedConn.open(route, context, params);
465                        } else {
466                            // otherwise give up
467                            throw ex;
468                        }
469
470                    }
471
472                }
473
474                // Run response protocol interceptors
475                response.setParams(params);
476                requestExec.postProcess(response, httpProcessor, context);
477
478
479                // The connection is in or can be brought to a re-usable state.
480                reuse = reuseStrategy.keepAlive(response, context);
481                if(reuse) {
482                    // Set the idle duration of this connection
483                    long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
484                    managedConn.setIdleDuration(duration, TimeUnit.MILLISECONDS);
485                }
486
487                RoutedRequest followup = handleResponse(roureq, response, context);
488                if (followup == null) {
489                    done = true;
490                } else {
491                    if (reuse) {
492                        this.log.debug("Connection kept alive");
493                        // Make sure the response body is fully consumed, if present
494                        HttpEntity entity = response.getEntity();
495                        if (entity != null) {
496                            entity.consumeContent();
497                        }
498                        // entity consumed above is not an auto-release entity,
499                        // need to mark the connection re-usable explicitly
500                        managedConn.markReusable();
501                    } else {
502                        managedConn.close();
503                    }
504                    // check if we can use the same connection for the followup
505                    if (!followup.getRoute().equals(roureq.getRoute())) {
506                        releaseConnection();
507                    }
508                    roureq = followup;
509                }
510
511                userToken = this.userTokenHandler.getUserToken(context);
512                context.setAttribute(ClientContext.USER_TOKEN, userToken);
513                if (managedConn != null) {
514                    managedConn.setState(userToken);
515                }
516            } // while not done
517
518
519            // check for entity, release connection if possible
520            if ((response == null) || (response.getEntity() == null) ||
521                !response.getEntity().isStreaming()) {
522                // connection not needed and (assumed to be) in re-usable state
523                if (reuse)
524                    managedConn.markReusable();
525                releaseConnection();
526            } else {
527                // install an auto-release entity
528                HttpEntity entity = response.getEntity();
529                entity = new BasicManagedEntity(entity, managedConn, reuse);
530                response.setEntity(entity);
531            }
532
533            return response;
534
535        } catch (HttpException ex) {
536            abortConnection();
537            throw ex;
538        } catch (IOException ex) {
539            abortConnection();
540            throw ex;
541        } catch (RuntimeException ex) {
542            abortConnection();
543            throw ex;
544        }
545    } // execute
546
547    /**
548     * Returns the connection back to the connection manager
549     * and prepares for retrieving a new connection during
550     * the next request.
551     */
552    protected void releaseConnection() {
553        // Release the connection through the ManagedConnection instead of the
554        // ConnectionManager directly.  This lets the connection control how
555        // it is released.
556        try {
557            managedConn.releaseConnection();
558        } catch(IOException ignored) {
559            this.log.debug("IOException releasing connection", ignored);
560        }
561        managedConn = null;
562    }
563
564    /**
565     * Determines the route for a request.
566     * Called by {@link #execute}
567     * to determine the route for either the original or a followup request.
568     *
569     * @param target    the target host for the request.
570     *                  Implementations may accept <code>null</code>
571     *                  if they can still determine a route, for example
572     *                  to a default target or by inspecting the request.
573     * @param request   the request to execute
574     * @param context   the context to use for the execution,
575     *                  never <code>null</code>
576     *
577     * @return  the route the request should take
578     *
579     * @throws HttpException    in case of a problem
580     */
581    protected HttpRoute determineRoute(HttpHost    target,
582                                           HttpRequest request,
583                                           HttpContext context)
584        throws HttpException {
585
586        if (target == null) {
587            target = (HttpHost) request.getParams().getParameter(
588                ClientPNames.DEFAULT_HOST);
589        }
590        if (target == null) {
591            // BEGIN android-changed
592            //     If the URI was malformed, make it obvious where there's no host component
593            String scheme = null;
594            String host = null;
595            String path = null;
596            URI uri;
597            if (request instanceof HttpUriRequest
598                    && (uri = ((HttpUriRequest) request).getURI()) != null) {
599                scheme = uri.getScheme();
600                host = uri.getHost();
601                path = uri.getPath();
602            }
603            throw new IllegalStateException( "Target host must not be null, or set in parameters."
604                    + " scheme=" + scheme + ", host=" + host + ", path=" + path);
605            // END android-changed
606        }
607
608        return this.routePlanner.determineRoute(target, request, context);
609    }
610
611
612    /**
613     * Establishes the target route.
614     *
615     * @param route     the route to establish
616     * @param context   the context for the request execution
617     *
618     * @throws HttpException    in case of a problem
619     * @throws IOException      in case of an IO problem
620     */
621    protected void establishRoute(HttpRoute route, HttpContext context)
622        throws HttpException, IOException {
623
624        //@@@ how to handle CONNECT requests for tunnelling?
625        //@@@ refuse to send external CONNECT via director? special handling?
626
627        //@@@ should the request parameters already be used below?
628        //@@@ probably yes, but they're not linked yet
629        //@@@ will linking above cause problems with linking in reqExec?
630        //@@@ probably not, because the parent is replaced
631        //@@@ just make sure we don't link parameters to themselves
632
633        HttpRouteDirector rowdy = new BasicRouteDirector();
634        int step;
635        do {
636            HttpRoute fact = managedConn.getRoute();
637            step = rowdy.nextStep(route, fact);
638
639            switch (step) {
640
641            case HttpRouteDirector.CONNECT_TARGET:
642            case HttpRouteDirector.CONNECT_PROXY:
643                managedConn.open(route, context, this.params);
644                break;
645
646            case HttpRouteDirector.TUNNEL_TARGET: {
647                boolean secure = createTunnelToTarget(route, context);
648                this.log.debug("Tunnel to target created.");
649                managedConn.tunnelTarget(secure, this.params);
650            }   break;
651
652            case HttpRouteDirector.TUNNEL_PROXY: {
653                // The most simple example for this case is a proxy chain
654                // of two proxies, where P1 must be tunnelled to P2.
655                // route: Source -> P1 -> P2 -> Target (3 hops)
656                // fact:  Source -> P1 -> Target       (2 hops)
657                final int hop = fact.getHopCount()-1; // the hop to establish
658                boolean secure = createTunnelToProxy(route, hop, context);
659                this.log.debug("Tunnel to proxy created.");
660                managedConn.tunnelProxy(route.getHopTarget(hop),
661                                        secure, this.params);
662            }   break;
663
664
665            case HttpRouteDirector.LAYER_PROTOCOL:
666                managedConn.layerProtocol(context, this.params);
667                break;
668
669            case HttpRouteDirector.UNREACHABLE:
670                throw new IllegalStateException
671                    ("Unable to establish route." +
672                     "\nplanned = " + route +
673                     "\ncurrent = " + fact);
674
675            case HttpRouteDirector.COMPLETE:
676                // do nothing
677                break;
678
679            default:
680                throw new IllegalStateException
681                    ("Unknown step indicator "+step+" from RouteDirector.");
682            } // switch
683
684        } while (step > HttpRouteDirector.COMPLETE);
685
686    } // establishConnection
687
688
689    /**
690     * Creates a tunnel to the target server.
691     * The connection must be established to the (last) proxy.
692     * A CONNECT request for tunnelling through the proxy will
693     * be created and sent, the response received and checked.
694     * This method does <i>not</i> update the connection with
695     * information about the tunnel, that is left to the caller.
696     *
697     * @param route     the route to establish
698     * @param context   the context for request execution
699     *
700     * @return  <code>true</code> if the tunnelled route is secure,
701     *          <code>false</code> otherwise.
702     *          The implementation here always returns <code>false</code>,
703     *          but derived classes may override.
704     *
705     * @throws HttpException    in case of a problem
706     * @throws IOException      in case of an IO problem
707     */
708    protected boolean createTunnelToTarget(HttpRoute route,
709                                           HttpContext context)
710        throws HttpException, IOException {
711
712        HttpHost proxy = route.getProxyHost();
713        HttpHost target = route.getTargetHost();
714        HttpResponse response = null;
715
716        boolean done = false;
717        while (!done) {
718
719            done = true;
720
721            if (!this.managedConn.isOpen()) {
722                this.managedConn.open(route, context, this.params);
723            }
724
725            HttpRequest connect = createConnectRequest(route, context);
726
727            String agent = HttpProtocolParams.getUserAgent(params);
728            if (agent != null) {
729                connect.addHeader(HTTP.USER_AGENT, agent);
730            }
731            connect.addHeader(HTTP.TARGET_HOST, target.toHostString());
732
733            AuthScheme authScheme = this.proxyAuthState.getAuthScheme();
734            AuthScope authScope = this.proxyAuthState.getAuthScope();
735            Credentials creds = this.proxyAuthState.getCredentials();
736            if (creds != null) {
737                if (authScope != null || !authScheme.isConnectionBased()) {
738                    try {
739                        connect.addHeader(authScheme.authenticate(creds, connect));
740                    } catch (AuthenticationException ex) {
741                        if (this.log.isErrorEnabled()) {
742                            this.log.error("Proxy authentication error: " + ex.getMessage());
743                        }
744                    }
745                }
746            }
747
748            response = requestExec.execute(connect, this.managedConn, context);
749
750            int status = response.getStatusLine().getStatusCode();
751            if (status < 200) {
752                throw new HttpException("Unexpected response to CONNECT request: " +
753                        response.getStatusLine());
754            }
755
756            CredentialsProvider credsProvider = (CredentialsProvider)
757                context.getAttribute(ClientContext.CREDS_PROVIDER);
758
759            if (credsProvider != null && HttpClientParams.isAuthenticating(params)) {
760                if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) {
761
762                    this.log.debug("Proxy requested authentication");
763                    Map<String, Header> challenges = this.proxyAuthHandler.getChallenges(
764                            response, context);
765                    try {
766                        processChallenges(
767                                challenges, this.proxyAuthState, this.proxyAuthHandler,
768                                response, context);
769                    } catch (AuthenticationException ex) {
770                        if (this.log.isWarnEnabled()) {
771                            this.log.warn("Authentication error: " +  ex.getMessage());
772                            break;
773                        }
774                    }
775                    updateAuthState(this.proxyAuthState, proxy, credsProvider);
776
777                    if (this.proxyAuthState.getCredentials() != null) {
778                        done = false;
779
780                        // Retry request
781                        if (this.reuseStrategy.keepAlive(response, context)) {
782                            this.log.debug("Connection kept alive");
783                            // Consume response content
784                            HttpEntity entity = response.getEntity();
785                            if (entity != null) {
786                                entity.consumeContent();
787                            }
788                        } else {
789                            this.managedConn.close();
790                        }
791
792                    }
793
794                } else {
795                    // Reset proxy auth scope
796                    this.proxyAuthState.setAuthScope(null);
797                }
798            }
799        }
800
801        int status = response.getStatusLine().getStatusCode();
802
803        if (status > 299) {
804
805            // Buffer response content
806            HttpEntity entity = response.getEntity();
807            if (entity != null) {
808                response.setEntity(new BufferedHttpEntity(entity));
809            }
810
811            this.managedConn.close();
812            throw new TunnelRefusedException("CONNECT refused by proxy: " +
813                    response.getStatusLine(), response);
814        }
815
816        this.managedConn.markReusable();
817
818        // How to decide on security of the tunnelled connection?
819        // The socket factory knows only about the segment to the proxy.
820        // Even if that is secure, the hop to the target may be insecure.
821        // Leave it to derived classes, consider insecure by default here.
822        return false;
823
824    } // createTunnelToTarget
825
826
827
828    /**
829     * Creates a tunnel to an intermediate proxy.
830     * This method is <i>not</i> implemented in this class.
831     * It just throws an exception here.
832     *
833     * @param route     the route to establish
834     * @param hop       the hop in the route to establish now.
835     *                  <code>route.getHopTarget(hop)</code>
836     *                  will return the proxy to tunnel to.
837     * @param context   the context for request execution
838     *
839     * @return  <code>true</code> if the partially tunnelled connection
840     *          is secure, <code>false</code> otherwise.
841     *
842     * @throws HttpException    in case of a problem
843     * @throws IOException      in case of an IO problem
844     */
845    protected boolean createTunnelToProxy(HttpRoute route, int hop,
846                                          HttpContext context)
847        throws HttpException, IOException {
848
849        // Have a look at createTunnelToTarget and replicate the parts
850        // you need in a custom derived class. If your proxies don't require
851        // authentication, it is not too hard. But for the stock version of
852        // HttpClient, we cannot make such simplifying assumptions and would
853        // have to include proxy authentication code. The HttpComponents team
854        // is currently not in a position to support rarely used code of this
855        // complexity. Feel free to submit patches that refactor the code in
856        // createTunnelToTarget to facilitate re-use for proxy tunnelling.
857
858        throw new UnsupportedOperationException
859            ("Proxy chains are not supported.");
860    }
861
862
863
864    /**
865     * Creates the CONNECT request for tunnelling.
866     * Called by {@link #createTunnelToTarget createTunnelToTarget}.
867     *
868     * @param route     the route to establish
869     * @param context   the context for request execution
870     *
871     * @return  the CONNECT request for tunnelling
872     */
873    protected HttpRequest createConnectRequest(HttpRoute route,
874                                               HttpContext context) {
875        // see RFC 2817, section 5.2 and
876        // INTERNET-DRAFT: Tunneling TCP based protocols through
877        // Web proxy servers
878
879        HttpHost target = route.getTargetHost();
880
881        String host = target.getHostName();
882        int port = target.getPort();
883        if (port < 0) {
884            Scheme scheme = connManager.getSchemeRegistry().
885                getScheme(target.getSchemeName());
886            port = scheme.getDefaultPort();
887        }
888
889        StringBuilder buffer = new StringBuilder(host.length() + 6);
890        buffer.append(host);
891        buffer.append(':');
892        buffer.append(Integer.toString(port));
893
894        String authority = buffer.toString();
895        ProtocolVersion ver = HttpProtocolParams.getVersion(params);
896        HttpRequest req = new BasicHttpRequest
897            ("CONNECT", authority, ver);
898
899        return req;
900    }
901
902
903    /**
904     * Analyzes a response to check need for a followup.
905     *
906     * @param roureq    the request and route.
907     * @param response  the response to analayze
908     * @param context   the context used for the current request execution
909     *
910     * @return  the followup request and route if there is a followup, or
911     *          <code>null</code> if the response should be returned as is
912     *
913     * @throws HttpException    in case of a problem
914     * @throws IOException      in case of an IO problem
915     */
916    protected RoutedRequest handleResponse(RoutedRequest roureq,
917                                           HttpResponse response,
918                                           HttpContext context)
919        throws HttpException, IOException {
920
921        HttpRoute route = roureq.getRoute();
922        HttpHost proxy = route.getProxyHost();
923        RequestWrapper request = roureq.getRequest();
924
925        HttpParams params = request.getParams();
926        if (HttpClientParams.isRedirecting(params) &&
927                this.redirectHandler.isRedirectRequested(response, context)) {
928
929            if (redirectCount >= maxRedirects) {
930                throw new RedirectException("Maximum redirects ("
931                        + maxRedirects + ") exceeded");
932            }
933            redirectCount++;
934
935            URI uri = this.redirectHandler.getLocationURI(response, context);
936
937            HttpHost newTarget = new HttpHost(
938                    uri.getHost(),
939                    uri.getPort(),
940                    uri.getScheme());
941
942            HttpGet redirect = new HttpGet(uri);
943
944            HttpRequest orig = request.getOriginal();
945            redirect.setHeaders(orig.getAllHeaders());
946
947            RequestWrapper wrapper = new RequestWrapper(redirect);
948            wrapper.setParams(params);
949
950            HttpRoute newRoute = determineRoute(newTarget, wrapper, context);
951            RoutedRequest newRequest = new RoutedRequest(wrapper, newRoute);
952
953            if (this.log.isDebugEnabled()) {
954                this.log.debug("Redirecting to '" + uri + "' via " + newRoute);
955            }
956
957            return newRequest;
958        }
959
960        CredentialsProvider credsProvider = (CredentialsProvider)
961            context.getAttribute(ClientContext.CREDS_PROVIDER);
962
963        if (credsProvider != null && HttpClientParams.isAuthenticating(params)) {
964
965            if (this.targetAuthHandler.isAuthenticationRequested(response, context)) {
966
967                HttpHost target = (HttpHost)
968                    context.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
969                if (target == null) {
970                    target = route.getTargetHost();
971                }
972
973                this.log.debug("Target requested authentication");
974                Map<String, Header> challenges = this.targetAuthHandler.getChallenges(
975                        response, context);
976                try {
977                    processChallenges(challenges,
978                            this.targetAuthState, this.targetAuthHandler,
979                            response, context);
980                } catch (AuthenticationException ex) {
981                    if (this.log.isWarnEnabled()) {
982                        this.log.warn("Authentication error: " +  ex.getMessage());
983                        return null;
984                    }
985                }
986                updateAuthState(this.targetAuthState, target, credsProvider);
987
988                if (this.targetAuthState.getCredentials() != null) {
989                    // Re-try the same request via the same route
990                    return roureq;
991                } else {
992                    return null;
993                }
994            } else {
995                // Reset target auth scope
996                this.targetAuthState.setAuthScope(null);
997            }
998
999            if (this.proxyAuthHandler.isAuthenticationRequested(response, context)) {
1000
1001                this.log.debug("Proxy requested authentication");
1002                Map<String, Header> challenges = this.proxyAuthHandler.getChallenges(
1003                        response, context);
1004                try {
1005                    processChallenges(challenges,
1006                            this.proxyAuthState, this.proxyAuthHandler,
1007                            response, context);
1008                } catch (AuthenticationException ex) {
1009                    if (this.log.isWarnEnabled()) {
1010                        this.log.warn("Authentication error: " +  ex.getMessage());
1011                        return null;
1012                    }
1013                }
1014                updateAuthState(this.proxyAuthState, proxy, credsProvider);
1015
1016                if (this.proxyAuthState.getCredentials() != null) {
1017                    // Re-try the same request via the same route
1018                    return roureq;
1019                } else {
1020                    return null;
1021                }
1022            } else {
1023                // Reset proxy auth scope
1024                this.proxyAuthState.setAuthScope(null);
1025            }
1026        }
1027        return null;
1028    } // handleResponse
1029
1030
1031    /**
1032     * Shuts down the connection.
1033     * This method is called from a <code>catch</code> block in
1034     * {@link #execute execute} during exception handling.
1035     */
1036    private void abortConnection() {
1037        ManagedClientConnection mcc = managedConn;
1038        if (mcc != null) {
1039            // we got here as the result of an exception
1040            // no response will be returned, release the connection
1041            managedConn = null;
1042            try {
1043                mcc.abortConnection();
1044            } catch (IOException ex) {
1045                if (this.log.isDebugEnabled()) {
1046                    this.log.debug(ex.getMessage(), ex);
1047                }
1048            }
1049            // ensure the connection manager properly releases this connection
1050            try {
1051                mcc.releaseConnection();
1052            } catch(IOException ignored) {
1053                this.log.debug("Error releasing connection", ignored);
1054            }
1055        }
1056    } // abortConnection
1057
1058
1059    private void processChallenges(
1060            final Map<String, Header> challenges,
1061            final AuthState authState,
1062            final AuthenticationHandler authHandler,
1063            final HttpResponse response,
1064            final HttpContext context)
1065                throws MalformedChallengeException, AuthenticationException {
1066
1067        AuthScheme authScheme = authState.getAuthScheme();
1068        if (authScheme == null) {
1069            // Authentication not attempted before
1070            authScheme = authHandler.selectScheme(challenges, response, context);
1071            authState.setAuthScheme(authScheme);
1072        }
1073        String id = authScheme.getSchemeName();
1074
1075        Header challenge = challenges.get(id.toLowerCase(Locale.ENGLISH));
1076        if (challenge == null) {
1077            throw new AuthenticationException(id +
1078                " authorization challenge expected, but not found");
1079        }
1080        authScheme.processChallenge(challenge);
1081        this.log.debug("Authorization challenge processed");
1082    }
1083
1084
1085    private void updateAuthState(
1086            final AuthState authState,
1087            final HttpHost host,
1088            final CredentialsProvider credsProvider) {
1089
1090        if (!authState.isValid()) {
1091            return;
1092        }
1093
1094        String hostname = host.getHostName();
1095        int port = host.getPort();
1096        if (port < 0) {
1097            Scheme scheme = connManager.getSchemeRegistry().getScheme(host);
1098            port = scheme.getDefaultPort();
1099        }
1100
1101        AuthScheme authScheme = authState.getAuthScheme();
1102        AuthScope authScope = new AuthScope(
1103                hostname,
1104                port,
1105                authScheme.getRealm(),
1106                authScheme.getSchemeName());
1107
1108        if (this.log.isDebugEnabled()) {
1109            this.log.debug("Authentication scope: " + authScope);
1110        }
1111        Credentials creds = authState.getCredentials();
1112        if (creds == null) {
1113            creds = credsProvider.getCredentials(authScope);
1114            if (this.log.isDebugEnabled()) {
1115                if (creds != null) {
1116                    this.log.debug("Found credentials");
1117                } else {
1118                    this.log.debug("Credentials not found");
1119                }
1120            }
1121        } else {
1122            if (authScheme.isComplete()) {
1123                this.log.debug("Authentication failed");
1124                creds = null;
1125            }
1126        }
1127        authState.setAuthScope(authScope);
1128        authState.setCredentials(creds);
1129    }
1130
1131    // BEGIN android-added
1132    /** Cached instance of android.security.NetworkSecurityPolicy. */
1133    private static Object networkSecurityPolicy;
1134
1135    /** Cached android.security.NetworkSecurityPolicy.isCleartextTrafficPermitted method. */
1136    private static Method cleartextTrafficPermittedMethod;
1137
1138    private static boolean isCleartextTrafficPermitted() {
1139        // TODO: Remove this method once NetworkSecurityPolicy can be accessed without Reflection.
1140        // This method invokes NetworkSecurityPolicy.getInstance().isCleartextTrafficPermitted
1141        // via Reflection API.
1142        // Because of the way external/apache-http is built, in the near term it can't invoke new
1143        // Android framework API directly.
1144        try {
1145            Object policy;
1146            Method method;
1147            synchronized (DefaultRequestDirector.class) {
1148                if (cleartextTrafficPermittedMethod == null) {
1149                    Class<?> cls = Class.forName("android.security.NetworkSecurityPolicy");
1150                    Method getInstanceMethod = cls.getMethod("getInstance");
1151                    networkSecurityPolicy = getInstanceMethod.invoke(null);
1152                    cleartextTrafficPermittedMethod = cls.getMethod("isCleartextTrafficPermitted");
1153                }
1154                policy = networkSecurityPolicy;
1155                method = cleartextTrafficPermittedMethod;
1156            }
1157            return (Boolean) method.invoke(policy);
1158        } catch (ReflectiveOperationException e) {
1159            // Can't access the Android framework NetworkSecurityPolicy. To be backward compatible,
1160            // assume that cleartext traffic is permitted. Android CTS will take care of ensuring
1161            // this issue doesn't occur on new Android platforms.
1162            return true;
1163        }
1164    }
1165    // END android-added
1166
1167} // class DefaultClientRequestDirector
1168