View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.http.impl.execchain;
29  
30  import java.io.IOException;
31  import java.io.InterruptedIOException;
32  import java.util.concurrent.ExecutionException;
33  import java.util.concurrent.TimeUnit;
34  
35  import org.apache.commons.logging.Log;
36  import org.apache.commons.logging.LogFactory;
37  import org.apache.http.ConnectionReuseStrategy;
38  import org.apache.http.HttpClientConnection;
39  import org.apache.http.HttpEntity;
40  import org.apache.http.HttpEntityEnclosingRequest;
41  import org.apache.http.HttpException;
42  import org.apache.http.HttpHost;
43  import org.apache.http.HttpRequest;
44  import org.apache.http.HttpResponse;
45  import org.apache.http.annotation.Contract;
46  import org.apache.http.annotation.ThreadingBehavior;
47  import org.apache.http.auth.AUTH;
48  import org.apache.http.auth.AuthProtocolState;
49  import org.apache.http.auth.AuthState;
50  import org.apache.http.client.AuthenticationStrategy;
51  import org.apache.http.client.NonRepeatableRequestException;
52  import org.apache.http.client.UserTokenHandler;
53  import org.apache.http.client.config.RequestConfig;
54  import org.apache.http.client.methods.CloseableHttpResponse;
55  import org.apache.http.client.methods.HttpExecutionAware;
56  import org.apache.http.client.methods.HttpRequestWrapper;
57  import org.apache.http.client.protocol.HttpClientContext;
58  import org.apache.http.conn.ConnectionKeepAliveStrategy;
59  import org.apache.http.conn.ConnectionRequest;
60  import org.apache.http.conn.HttpClientConnectionManager;
61  import org.apache.http.conn.routing.BasicRouteDirector;
62  import org.apache.http.conn.routing.HttpRoute;
63  import org.apache.http.conn.routing.HttpRouteDirector;
64  import org.apache.http.conn.routing.RouteTracker;
65  import org.apache.http.entity.BufferedHttpEntity;
66  import org.apache.http.impl.auth.HttpAuthenticator;
67  import org.apache.http.impl.conn.ConnectionShutdownException;
68  import org.apache.http.message.BasicHttpRequest;
69  import org.apache.http.protocol.HttpCoreContext;
70  import org.apache.http.protocol.HttpProcessor;
71  import org.apache.http.protocol.HttpRequestExecutor;
72  import org.apache.http.protocol.ImmutableHttpProcessor;
73  import org.apache.http.protocol.RequestTargetHost;
74  import org.apache.http.util.Args;
75  import org.apache.http.util.EntityUtils;
76  
77  /**
78   * The last request executor in the HTTP request execution chain
79   * that is responsible for execution of request / response
80   * exchanges with the opposite endpoint.
81   * This executor will automatically retry the request in case
82   * of an authentication challenge by an intermediate proxy or
83   * by the target server.
84   *
85   * @since 4.3
86   */
87  @SuppressWarnings("deprecation")
88  @Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
89  public class MainClientExec implements ClientExecChain {
90  
91      private final Log log = LogFactory.getLog(getClass());
92  
93      private final HttpRequestExecutor requestExecutor;
94      private final HttpClientConnectionManager connManager;
95      private final ConnectionReuseStrategy reuseStrategy;
96      private final ConnectionKeepAliveStrategy keepAliveStrategy;
97      private final HttpProcessor proxyHttpProcessor;
98      private final AuthenticationStrategy targetAuthStrategy;
99      private final AuthenticationStrategy proxyAuthStrategy;
100     private final HttpAuthenticator authenticator;
101     private final UserTokenHandler userTokenHandler;
102     private final HttpRouteDirector routeDirector;
103 
104     /**
105      * @since 4.4
106      */
107     public MainClientExec(
108             final HttpRequestExecutor requestExecutor,
109             final HttpClientConnectionManager connManager,
110             final ConnectionReuseStrategy reuseStrategy,
111             final ConnectionKeepAliveStrategy keepAliveStrategy,
112             final HttpProcessor proxyHttpProcessor,
113             final AuthenticationStrategy targetAuthStrategy,
114             final AuthenticationStrategy proxyAuthStrategy,
115             final UserTokenHandler userTokenHandler) {
116         Args.notNull(requestExecutor, "HTTP request executor");
117         Args.notNull(connManager, "Client connection manager");
118         Args.notNull(reuseStrategy, "Connection reuse strategy");
119         Args.notNull(keepAliveStrategy, "Connection keep alive strategy");
120         Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
121         Args.notNull(targetAuthStrategy, "Target authentication strategy");
122         Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
123         Args.notNull(userTokenHandler, "User token handler");
124         this.authenticator      = new HttpAuthenticator();
125         this.routeDirector      = new BasicRouteDirector();
126         this.requestExecutor    = requestExecutor;
127         this.connManager        = connManager;
128         this.reuseStrategy      = reuseStrategy;
129         this.keepAliveStrategy  = keepAliveStrategy;
130         this.proxyHttpProcessor = proxyHttpProcessor;
131         this.targetAuthStrategy = targetAuthStrategy;
132         this.proxyAuthStrategy  = proxyAuthStrategy;
133         this.userTokenHandler   = userTokenHandler;
134     }
135 
136     public MainClientExec(
137             final HttpRequestExecutor requestExecutor,
138             final HttpClientConnectionManager connManager,
139             final ConnectionReuseStrategy reuseStrategy,
140             final ConnectionKeepAliveStrategy keepAliveStrategy,
141             final AuthenticationStrategy targetAuthStrategy,
142             final AuthenticationStrategy proxyAuthStrategy,
143             final UserTokenHandler userTokenHandler) {
144         this(requestExecutor, connManager, reuseStrategy, keepAliveStrategy,
145                 new ImmutableHttpProcessor(new RequestTargetHost()),
146                 targetAuthStrategy, proxyAuthStrategy, userTokenHandler);
147     }
148 
149     @Override
150     public CloseableHttpResponse execute(
151             final HttpRoute route,
152             final HttpRequestWrapper request,
153             final HttpClientContext context,
154             final HttpExecutionAware execAware) throws IOException, HttpException {
155         Args.notNull(route, "HTTP route");
156         Args.notNull(request, "HTTP request");
157         Args.notNull(context, "HTTP context");
158 
159         AuthState targetAuthState = context.getTargetAuthState();
160         if (targetAuthState == null) {
161             targetAuthState = new AuthState();
162             context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState);
163         }
164         AuthState proxyAuthState = context.getProxyAuthState();
165         if (proxyAuthState == null) {
166             proxyAuthState = new AuthState();
167             context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState);
168         }
169 
170         if (request instanceof HttpEntityEnclosingRequest) {
171             RequestEntityProxy.enhance((HttpEntityEnclosingRequest) request);
172         }
173 
174         Object userToken = context.getUserToken();
175 
176         final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
177         if (execAware != null) {
178             if (execAware.isAborted()) {
179                 connRequest.cancel();
180                 throw new RequestAbortedException("Request aborted");
181             } else {
182                 execAware.setCancellable(connRequest);
183             }
184         }
185 
186         final RequestConfig config = context.getRequestConfig();
187 
188         final HttpClientConnection managedConn;
189         try {
190             final int timeout = config.getConnectionRequestTimeout();
191             managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
192         } catch(final InterruptedException interrupted) {
193             Thread.currentThread().interrupt();
194             throw new RequestAbortedException("Request aborted", interrupted);
195         } catch(final ExecutionException ex) {
196             Throwable cause = ex.getCause();
197             if (cause == null) {
198                 cause = ex;
199             }
200             throw new RequestAbortedException("Request execution failed", cause);
201         }
202 
203         context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);
204 
205         if (config.isStaleConnectionCheckEnabled()) {
206             // validate connection
207             if (managedConn.isOpen()) {
208                 this.log.debug("Stale connection check");
209                 if (managedConn.isStale()) {
210                     this.log.debug("Stale connection detected");
211                     managedConn.close();
212                 }
213             }
214         }
215 
216         final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
217         try {
218             if (execAware != null) {
219                 execAware.setCancellable(connHolder);
220             }
221 
222             HttpResponse response;
223             for (int execCount = 1;; execCount++) {
224 
225                 if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) {
226                     throw new NonRepeatableRequestException("Cannot retry request " +
227                             "with a non-repeatable request entity.");
228                 }
229 
230                 if (execAware != null && execAware.isAborted()) {
231                     throw new RequestAbortedException("Request aborted");
232                 }
233 
234                 if (!managedConn.isOpen()) {
235                     this.log.debug("Opening connection " + route);
236                     try {
237                         establishRoute(proxyAuthState, managedConn, route, request, context);
238                     } catch (final TunnelRefusedException ex) {
239                         if (this.log.isDebugEnabled()) {
240                             this.log.debug(ex.getMessage());
241                         }
242                         response = ex.getResponse();
243                         break;
244                     }
245                 }
246                 final int timeout = config.getSocketTimeout();
247                 if (timeout >= 0) {
248                     managedConn.setSocketTimeout(timeout);
249                 }
250 
251                 if (execAware != null && execAware.isAborted()) {
252                     throw new RequestAbortedException("Request aborted");
253                 }
254 
255                 if (this.log.isDebugEnabled()) {
256                     this.log.debug("Executing request " + request.getRequestLine());
257                 }
258 
259                 if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) {
260                     if (this.log.isDebugEnabled()) {
261                         this.log.debug("Target auth state: " + targetAuthState.getState());
262                     }
263                     this.authenticator.generateAuthResponse(request, targetAuthState, context);
264                 }
265                 if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) {
266                     if (this.log.isDebugEnabled()) {
267                         this.log.debug("Proxy auth state: " + proxyAuthState.getState());
268                     }
269                     this.authenticator.generateAuthResponse(request, proxyAuthState, context);
270                 }
271 
272                 response = requestExecutor.execute(request, managedConn, context);
273 
274                 // The connection is in or can be brought to a re-usable state.
275                 if (reuseStrategy.keepAlive(response, context)) {
276                     // Set the idle duration of this connection
277                     final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
278                     if (this.log.isDebugEnabled()) {
279                         final String s;
280                         if (duration > 0) {
281                             s = "for " + duration + " " + TimeUnit.MILLISECONDS;
282                         } else {
283                             s = "indefinitely";
284                         }
285                         this.log.debug("Connection can be kept alive " + s);
286                     }
287                     connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
288                     connHolder.markReusable();
289                 } else {
290                     connHolder.markNonReusable();
291                 }
292 
293                 if (needAuthentication(
294                         targetAuthState, proxyAuthState, route, response, context)) {
295                     // Make sure the response body is fully consumed, if present
296                     final HttpEntity entity = response.getEntity();
297                     if (connHolder.isReusable()) {
298                         EntityUtils.consume(entity);
299                     } else {
300                         managedConn.close();
301                         if (proxyAuthState.getState() == AuthProtocolState.SUCCESS
302                                 && proxyAuthState.getAuthScheme() != null
303                                 && proxyAuthState.getAuthScheme().isConnectionBased()) {
304                             this.log.debug("Resetting proxy auth state");
305                             proxyAuthState.reset();
306                         }
307                         if (targetAuthState.getState() == AuthProtocolState.SUCCESS
308                                 && targetAuthState.getAuthScheme() != null
309                                 && targetAuthState.getAuthScheme().isConnectionBased()) {
310                             this.log.debug("Resetting target auth state");
311                             targetAuthState.reset();
312                         }
313                     }
314                     // discard previous auth headers
315                     final HttpRequest original = request.getOriginal();
316                     if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) {
317                         request.removeHeaders(AUTH.WWW_AUTH_RESP);
318                     }
319                     if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) {
320                         request.removeHeaders(AUTH.PROXY_AUTH_RESP);
321                     }
322                 } else {
323                     break;
324                 }
325             }
326 
327             if (userToken == null) {
328                 userToken = userTokenHandler.getUserToken(context);
329                 context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
330             }
331             if (userToken != null) {
332                 connHolder.setState(userToken);
333             }
334 
335             // check for entity, release connection if possible
336             final HttpEntity entity = response.getEntity();
337             if (entity == null || !entity.isStreaming()) {
338                 // connection not needed and (assumed to be) in re-usable state
339                 connHolder.releaseConnection();
340                 return new HttpResponseProxy(response, null);
341             } else {
342                 return new HttpResponseProxy(response, connHolder);
343             }
344         } catch (final ConnectionShutdownException ex) {
345             final InterruptedIOException ioex = new InterruptedIOException(
346                     "Connection has been shut down");
347             ioex.initCause(ex);
348             throw ioex;
349         } catch (final HttpException ex) {
350             connHolder.abortConnection();
351             throw ex;
352         } catch (final IOException ex) {
353             connHolder.abortConnection();
354             throw ex;
355         } catch (final RuntimeException ex) {
356             connHolder.abortConnection();
357             throw ex;
358         }
359     }
360 
361     /**
362      * Establishes the target route.
363      */
364     void establishRoute(
365             final AuthState proxyAuthState,
366             final HttpClientConnection managedConn,
367             final HttpRoute route,
368             final HttpRequest request,
369             final HttpClientContext context) throws HttpException, IOException {
370         final RequestConfig config = context.getRequestConfig();
371         final int timeout = config.getConnectTimeout();
372         final RouteTracker tracker = new RouteTracker(route);
373         int step;
374         do {
375             final HttpRoute fact = tracker.toRoute();
376             step = this.routeDirector.nextStep(route, fact);
377 
378             switch (step) {
379 
380             case HttpRouteDirector.CONNECT_TARGET:
381                 this.connManager.connect(
382                         managedConn,
383                         route,
384                         timeout > 0 ? timeout : 0,
385                         context);
386                 tracker.connectTarget(route.isSecure());
387                 break;
388             case HttpRouteDirector.CONNECT_PROXY:
389                 this.connManager.connect(
390                         managedConn,
391                         route,
392                         timeout > 0 ? timeout : 0,
393                         context);
394                 final HttpHost proxy  = route.getProxyHost();
395                 tracker.connectProxy(proxy, false);
396                 break;
397             case HttpRouteDirector.TUNNEL_TARGET: {
398                 final boolean secure = createTunnelToTarget(
399                         proxyAuthState, managedConn, route, request, context);
400                 this.log.debug("Tunnel to target created.");
401                 tracker.tunnelTarget(secure);
402             }   break;
403 
404             case HttpRouteDirector.TUNNEL_PROXY: {
405                 // The most simple example for this case is a proxy chain
406                 // of two proxies, where P1 must be tunnelled to P2.
407                 // route: Source -> P1 -> P2 -> Target (3 hops)
408                 // fact:  Source -> P1 -> Target       (2 hops)
409                 final int hop = fact.getHopCount()-1; // the hop to establish
410                 final boolean secure = createTunnelToProxy(route, hop, context);
411                 this.log.debug("Tunnel to proxy created.");
412                 tracker.tunnelProxy(route.getHopTarget(hop), secure);
413             }   break;
414 
415             case HttpRouteDirector.LAYER_PROTOCOL:
416                 this.connManager.upgrade(managedConn, route, context);
417                 tracker.layerProtocol(route.isSecure());
418                 break;
419 
420             case HttpRouteDirector.UNREACHABLE:
421                 throw new HttpException("Unable to establish route: " +
422                         "planned = " + route + "; current = " + fact);
423             case HttpRouteDirector.COMPLETE:
424                 this.connManager.routeComplete(managedConn, route, context);
425                 break;
426             default:
427                 throw new IllegalStateException("Unknown step indicator "
428                         + step + " from RouteDirector.");
429             }
430 
431         } while (step > HttpRouteDirector.COMPLETE);
432     }
433 
434     /**
435      * Creates a tunnel to the target server.
436      * The connection must be established to the (last) proxy.
437      * A CONNECT request for tunnelling through the proxy will
438      * be created and sent, the response received and checked.
439      * This method does <i>not</i> update the connection with
440      * information about the tunnel, that is left to the caller.
441      */
442     private boolean createTunnelToTarget(
443             final AuthState proxyAuthState,
444             final HttpClientConnection managedConn,
445             final HttpRoute route,
446             final HttpRequest request,
447             final HttpClientContext context) throws HttpException, IOException {
448 
449         final RequestConfig config = context.getRequestConfig();
450         final int timeout = config.getConnectTimeout();
451 
452         final HttpHost target = route.getTargetHost();
453         final HttpHost proxy = route.getProxyHost();
454         HttpResponse response = null;
455 
456         final String authority = target.toHostString();
457         final HttpRequest connect = new BasicHttpRequest("CONNECT", authority, request.getProtocolVersion());
458 
459         this.requestExecutor.preProcess(connect, this.proxyHttpProcessor, context);
460 
461         while (response == null) {
462             if (!managedConn.isOpen()) {
463                 this.connManager.connect(
464                         managedConn,
465                         route,
466                         timeout > 0 ? timeout : 0,
467                         context);
468             }
469 
470             connect.removeHeaders(AUTH.PROXY_AUTH_RESP);
471             this.authenticator.generateAuthResponse(connect, proxyAuthState, context);
472 
473             response = this.requestExecutor.execute(connect, managedConn, context);
474 
475             final int status = response.getStatusLine().getStatusCode();
476             if (status < 200) {
477                 throw new HttpException("Unexpected response to CONNECT request: " +
478                         response.getStatusLine());
479             }
480 
481             if (config.isAuthenticationEnabled()) {
482                 if (this.authenticator.isAuthenticationRequested(proxy, response,
483                         this.proxyAuthStrategy, proxyAuthState, context)) {
484                     if (this.authenticator.handleAuthChallenge(proxy, response,
485                             this.proxyAuthStrategy, proxyAuthState, context)) {
486                         // Retry request
487                         if (this.reuseStrategy.keepAlive(response, context)) {
488                             this.log.debug("Connection kept alive");
489                             // Consume response content
490                             final HttpEntity entity = response.getEntity();
491                             EntityUtils.consume(entity);
492                         } else {
493                             managedConn.close();
494                         }
495                         response = null;
496                     }
497                 }
498             }
499         }
500 
501         final int status = response.getStatusLine().getStatusCode();
502 
503         if (status > 299) {
504 
505             // Buffer response content
506             final HttpEntity entity = response.getEntity();
507             if (entity != null) {
508                 response.setEntity(new BufferedHttpEntity(entity));
509             }
510 
511             managedConn.close();
512             throw new TunnelRefusedException("CONNECT refused by proxy: " +
513                     response.getStatusLine(), response);
514         }
515 
516         // How to decide on security of the tunnelled connection?
517         // The socket factory knows only about the segment to the proxy.
518         // Even if that is secure, the hop to the target may be insecure.
519         // Leave it to derived classes, consider insecure by default here.
520         return false;
521     }
522 
523     /**
524      * Creates a tunnel to an intermediate proxy.
525      * This method is <i>not</i> implemented in this class.
526      * It just throws an exception here.
527      */
528     private boolean createTunnelToProxy(
529             final HttpRoute route,
530             final int hop,
531             final HttpClientContext context) throws HttpException {
532 
533         // Have a look at createTunnelToTarget and replicate the parts
534         // you need in a custom derived class. If your proxies don't require
535         // authentication, it is not too hard. But for the stock version of
536         // HttpClient, we cannot make such simplifying assumptions and would
537         // have to include proxy authentication code. The HttpComponents team
538         // is currently not in a position to support rarely used code of this
539         // complexity. Feel free to submit patches that refactor the code in
540         // createTunnelToTarget to facilitate re-use for proxy tunnelling.
541 
542         throw new HttpException("Proxy chains are not supported.");
543     }
544 
545     private boolean needAuthentication(
546             final AuthState targetAuthState,
547             final AuthState proxyAuthState,
548             final HttpRoute route,
549             final HttpResponse response,
550             final HttpClientContext context) {
551         final RequestConfig config = context.getRequestConfig();
552         if (config.isAuthenticationEnabled()) {
553             HttpHost target = context.getTargetHost();
554             if (target == null) {
555                 target = route.getTargetHost();
556             }
557             if (target.getPort() < 0) {
558                 target = new HttpHost(
559                         target.getHostName(),
560                         route.getTargetHost().getPort(),
561                         target.getSchemeName());
562             }
563             final boolean targetAuthRequested = this.authenticator.isAuthenticationRequested(
564                     target, response, this.targetAuthStrategy, targetAuthState, context);
565 
566             HttpHost proxy = route.getProxyHost();
567             // if proxy is not set use target host instead
568             if (proxy == null) {
569                 proxy = route.getTargetHost();
570             }
571             final boolean proxyAuthRequested = this.authenticator.isAuthenticationRequested(
572                     proxy, response, this.proxyAuthStrategy, proxyAuthState, context);
573 
574             if (targetAuthRequested) {
575                 return this.authenticator.handleAuthChallenge(target, response,
576                         this.targetAuthStrategy, targetAuthState, context);
577             }
578             if (proxyAuthRequested) {
579                 return this.authenticator.handleAuthChallenge(proxy, response,
580                         this.proxyAuthStrategy, proxyAuthState, context);
581             }
582         }
583         return false;
584     }
585 
586 }