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 
32 package org.apache.http.impl.client;
33 
34 import java.io.IOException;
35 import java.io.InterruptedIOException;
36 import java.lang.reflect.Method;
37 import java.net.URI;
38 import java.net.URISyntaxException;
39 import java.util.Locale;
40 import java.util.Map;
41 import java.util.concurrent.TimeUnit;
42 
43 import org.apache.commons.logging.Log;
44 import org.apache.commons.logging.LogFactory;
45 import org.apache.http.ConnectionReuseStrategy;
46 import org.apache.http.Header;
47 import org.apache.http.HttpEntity;
48 import org.apache.http.HttpEntityEnclosingRequest;
49 import org.apache.http.HttpException;
50 import org.apache.http.HttpHost;
51 import org.apache.http.HttpRequest;
52 import org.apache.http.HttpResponse;
53 import org.apache.http.ProtocolException;
54 import org.apache.http.ProtocolVersion;
55 import org.apache.http.auth.AuthScheme;
56 import org.apache.http.auth.AuthScope;
57 import org.apache.http.auth.AuthState;
58 import org.apache.http.auth.AuthenticationException;
59 import org.apache.http.auth.Credentials;
60 import org.apache.http.auth.MalformedChallengeException;
61 import org.apache.http.client.AuthenticationHandler;
62 import org.apache.http.client.RequestDirector;
63 import org.apache.http.client.CredentialsProvider;
64 import org.apache.http.client.HttpRequestRetryHandler;
65 import org.apache.http.client.NonRepeatableRequestException;
66 import org.apache.http.client.RedirectException;
67 import org.apache.http.client.RedirectHandler;
68 import org.apache.http.client.UserTokenHandler;
69 import org.apache.http.client.methods.AbortableHttpRequest;
70 import org.apache.http.client.methods.HttpGet;
71 import org.apache.http.client.methods.HttpUriRequest;
72 import org.apache.http.client.params.ClientPNames;
73 import org.apache.http.client.params.HttpClientParams;
74 import org.apache.http.client.protocol.ClientContext;
75 import org.apache.http.client.utils.URIUtils;
76 import org.apache.http.conn.BasicManagedEntity;
77 import org.apache.http.conn.ClientConnectionManager;
78 import org.apache.http.conn.ClientConnectionRequest;
79 import org.apache.http.conn.ConnectionKeepAliveStrategy;
80 import org.apache.http.conn.ManagedClientConnection;
81 import org.apache.http.conn.params.ConnManagerParams;
82 import org.apache.http.conn.routing.BasicRouteDirector;
83 import org.apache.http.conn.routing.HttpRoute;
84 import org.apache.http.conn.routing.HttpRouteDirector;
85 import org.apache.http.conn.routing.HttpRoutePlanner;
86 import org.apache.http.conn.scheme.Scheme;
87 import org.apache.http.entity.BufferedHttpEntity;
88 import org.apache.http.message.BasicHttpRequest;
89 import org.apache.http.params.HttpConnectionParams;
90 import org.apache.http.params.HttpParams;
91 import org.apache.http.params.HttpProtocolParams;
92 import org.apache.http.protocol.ExecutionContext;
93 import org.apache.http.protocol.HTTP;
94 import org.apache.http.protocol.HttpContext;
95 import org.apache.http.protocol.HttpProcessor;
96 import 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
116 public 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 
DefaultRequestDirector( final HttpRequestExecutor requestExec, final ClientConnectionManager conman, final ConnectionReuseStrategy reustrat, final ConnectionKeepAliveStrategy kastrat, final HttpRoutePlanner rouplan, final HttpProcessor httpProcessor, final HttpRequestRetryHandler retryHandler, final RedirectHandler redirectHandler, final AuthenticationHandler targetAuthHandler, final AuthenticationHandler proxyAuthHandler, final UserTokenHandler userTokenHandler, final HttpParams params)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 
wrapRequest( final HttpRequest request)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 
rewriteRequestURI( final RequestWrapper request, final HttpRoute route)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
execute(HttpHost target, HttpRequest request, HttpContext context)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      */
releaseConnection()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      */
determineRoute(HttpHost target, HttpRequest request, HttpContext context)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      */
establishRoute(HttpRoute route, HttpContext context)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      */
createTunnelToTarget(HttpRoute route, HttpContext context)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      */
createTunnelToProxy(HttpRoute route, int hop, HttpContext context)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      */
createConnectRequest(HttpRoute route, HttpContext context)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      */
handleResponse(RoutedRequest roureq, HttpResponse response, HttpContext context)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      */
abortConnection()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 
processChallenges( final Map<String, Header> challenges, final AuthState authState, final AuthenticationHandler authHandler, final HttpResponse response, final HttpContext context)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 
updateAuthState( final AuthState authState, final HttpHost host, final CredentialsProvider credsProvider)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 
isCleartextTrafficPermitted(String hostname)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