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