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.isConnectionBased()) {
303                             this.log.debug("Resetting proxy auth state");
304                             proxyAuthState.reset();
305                         }
306                         if (targetAuthState.getState() == AuthProtocolState.SUCCESS
307                                 && targetAuthState.isConnectionBased()) {
308                             this.log.debug("Resetting target auth state");
309                             targetAuthState.reset();
310                         }
311                     }
312                     // discard previous auth headers
313                     final HttpRequest original = request.getOriginal();
314                     if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) {
315                         request.removeHeaders(AUTH.WWW_AUTH_RESP);
316                     }
317                     if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) {
318                         request.removeHeaders(AUTH.PROXY_AUTH_RESP);
319                     }
320                 } else {
321                     break;
322                 }
323             }
324 
325             if (userToken == null) {
326                 userToken = userTokenHandler.getUserToken(context);
327                 context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
328             }
329             if (userToken != null) {
330                 connHolder.setState(userToken);
331             }
332 
333             // check for entity, release connection if possible
334             final HttpEntity entity = response.getEntity();
335             if (entity == null || !entity.isStreaming()) {
336                 // connection not needed and (assumed to be) in re-usable state
337                 connHolder.releaseConnection();
338                 return new HttpResponseProxy(response, null);
339             } else {
340                 return new HttpResponseProxy(response, connHolder);
341             }
342         } catch (final ConnectionShutdownException ex) {
343             final InterruptedIOException ioex = new InterruptedIOException(
344                     "Connection has been shut down");
345             ioex.initCause(ex);
346             throw ioex;
347         } catch (final HttpException ex) {
348             connHolder.abortConnection();
349             throw ex;
350         } catch (final IOException ex) {
351             connHolder.abortConnection();
352             if (proxyAuthState.isConnectionBased()) {
353                 proxyAuthState.reset();
354             }
355             if (targetAuthState.isConnectionBased()) {
356                 targetAuthState.reset();
357             }
358             throw ex;
359         } catch (final RuntimeException ex) {
360             connHolder.abortConnection();
361             if (proxyAuthState.isConnectionBased()) {
362                 proxyAuthState.reset();
363             }
364             if (targetAuthState.isConnectionBased()) {
365                 targetAuthState.reset();
366             }
367             throw ex;
368         } catch (final Error error) {
369             connManager.shutdown();
370             throw error;
371         }
372     }
373 
374     /**
375      * Establishes the target route.
376      */
377     void establishRoute(
378             final AuthState proxyAuthState,
379             final HttpClientConnection managedConn,
380             final HttpRoute route,
381             final HttpRequest request,
382             final HttpClientContext context) throws HttpException, IOException {
383         final RequestConfig config = context.getRequestConfig();
384         final int timeout = config.getConnectTimeout();
385         final RouteTracker tracker = new RouteTracker(route);
386         int step;
387         do {
388             final HttpRoute fact = tracker.toRoute();
389             step = this.routeDirector.nextStep(route, fact);
390 
391             switch (step) {
392 
393             case HttpRouteDirector.CONNECT_TARGET:
394                 this.connManager.connect(
395                         managedConn,
396                         route,
397                         timeout > 0 ? timeout : 0,
398                         context);
399                 tracker.connectTarget(route.isSecure());
400                 break;
401             case HttpRouteDirector.CONNECT_PROXY:
402                 this.connManager.connect(
403                         managedConn,
404                         route,
405                         timeout > 0 ? timeout : 0,
406                         context);
407                 final HttpHost proxy  = route.getProxyHost();
408                 tracker.connectProxy(proxy, false);
409                 break;
410             case HttpRouteDirector.TUNNEL_TARGET: {
411                 final boolean secure = createTunnelToTarget(
412                         proxyAuthState, managedConn, route, request, context);
413                 this.log.debug("Tunnel to target created.");
414                 tracker.tunnelTarget(secure);
415             }   break;
416 
417             case HttpRouteDirector.TUNNEL_PROXY: {
418                 // The most simple example for this case is a proxy chain
419                 // of two proxies, where P1 must be tunnelled to P2.
420                 // route: Source -> P1 -> P2 -> Target (3 hops)
421                 // fact:  Source -> P1 -> Target       (2 hops)
422                 final int hop = fact.getHopCount()-1; // the hop to establish
423                 final boolean secure = createTunnelToProxy(route, hop, context);
424                 this.log.debug("Tunnel to proxy created.");
425                 tracker.tunnelProxy(route.getHopTarget(hop), secure);
426             }   break;
427 
428             case HttpRouteDirector.LAYER_PROTOCOL:
429                 this.connManager.upgrade(managedConn, route, context);
430                 tracker.layerProtocol(route.isSecure());
431                 break;
432 
433             case HttpRouteDirector.UNREACHABLE:
434                 throw new HttpException("Unable to establish route: " +
435                         "planned = " + route + "; current = " + fact);
436             case HttpRouteDirector.COMPLETE:
437                 this.connManager.routeComplete(managedConn, route, context);
438                 break;
439             default:
440                 throw new IllegalStateException("Unknown step indicator "
441                         + step + " from RouteDirector.");
442             }
443 
444         } while (step > HttpRouteDirector.COMPLETE);
445     }
446 
447     /**
448      * Creates a tunnel to the target server.
449      * The connection must be established to the (last) proxy.
450      * A CONNECT request for tunnelling through the proxy will
451      * be created and sent, the response received and checked.
452      * This method does <i>not</i> update the connection with
453      * information about the tunnel, that is left to the caller.
454      */
455     private boolean createTunnelToTarget(
456             final AuthState proxyAuthState,
457             final HttpClientConnection managedConn,
458             final HttpRoute route,
459             final HttpRequest request,
460             final HttpClientContext context) throws HttpException, IOException {
461 
462         final RequestConfig config = context.getRequestConfig();
463         final int timeout = config.getConnectTimeout();
464 
465         final HttpHost target = route.getTargetHost();
466         final HttpHost proxy = route.getProxyHost();
467         HttpResponse response = null;
468 
469         final String authority = target.toHostString();
470         final HttpRequest connect = new BasicHttpRequest("CONNECT", authority, request.getProtocolVersion());
471 
472         this.requestExecutor.preProcess(connect, this.proxyHttpProcessor, context);
473 
474         while (response == null) {
475             if (!managedConn.isOpen()) {
476                 this.connManager.connect(
477                         managedConn,
478                         route,
479                         timeout > 0 ? timeout : 0,
480                         context);
481             }
482 
483             connect.removeHeaders(AUTH.PROXY_AUTH_RESP);
484             this.authenticator.generateAuthResponse(connect, proxyAuthState, context);
485 
486             response = this.requestExecutor.execute(connect, managedConn, context);
487             this.requestExecutor.postProcess(response, this.proxyHttpProcessor, context);
488 
489             final int status = response.getStatusLine().getStatusCode();
490             if (status < 200) {
491                 throw new HttpException("Unexpected response to CONNECT request: " +
492                         response.getStatusLine());
493             }
494 
495             if (config.isAuthenticationEnabled()) {
496                 if (this.authenticator.isAuthenticationRequested(proxy, response,
497                         this.proxyAuthStrategy, proxyAuthState, context)) {
498                     if (this.authenticator.handleAuthChallenge(proxy, response,
499                             this.proxyAuthStrategy, proxyAuthState, context)) {
500                         // Retry request
501                         if (this.reuseStrategy.keepAlive(response, context)) {
502                             this.log.debug("Connection kept alive");
503                             // Consume response content
504                             final HttpEntity entity = response.getEntity();
505                             EntityUtils.consume(entity);
506                         } else {
507                             managedConn.close();
508                         }
509                         response = null;
510                     }
511                 }
512             }
513         }
514 
515         final int status = response.getStatusLine().getStatusCode();
516 
517         if (status > 299) {
518 
519             // Buffer response content
520             final HttpEntity entity = response.getEntity();
521             if (entity != null) {
522                 response.setEntity(new BufferedHttpEntity(entity));
523             }
524 
525             managedConn.close();
526             throw new TunnelRefusedException("CONNECT refused by proxy: " +
527                     response.getStatusLine(), response);
528         }
529 
530         // How to decide on security of the tunnelled connection?
531         // The socket factory knows only about the segment to the proxy.
532         // Even if that is secure, the hop to the target may be insecure.
533         // Leave it to derived classes, consider insecure by default here.
534         return false;
535     }
536 
537     /**
538      * Creates a tunnel to an intermediate proxy.
539      * This method is <i>not</i> implemented in this class.
540      * It just throws an exception here.
541      */
542     private boolean createTunnelToProxy(
543             final HttpRoute route,
544             final int hop,
545             final HttpClientContext context) throws HttpException {
546 
547         // Have a look at createTunnelToTarget and replicate the parts
548         // you need in a custom derived class. If your proxies don't require
549         // authentication, it is not too hard. But for the stock version of
550         // HttpClient, we cannot make such simplifying assumptions and would
551         // have to include proxy authentication code. The HttpComponents team
552         // is currently not in a position to support rarely used code of this
553         // complexity. Feel free to submit patches that refactor the code in
554         // createTunnelToTarget to facilitate re-use for proxy tunnelling.
555 
556         throw new HttpException("Proxy chains are not supported.");
557     }
558 
559     private boolean needAuthentication(
560             final AuthState targetAuthState,
561             final AuthState proxyAuthState,
562             final HttpRoute route,
563             final HttpResponse response,
564             final HttpClientContext context) {
565         final RequestConfig config = context.getRequestConfig();
566         if (config.isAuthenticationEnabled()) {
567             HttpHost target = context.getTargetHost();
568             if (target == null) {
569                 target = route.getTargetHost();
570             }
571             if (target.getPort() < 0) {
572                 target = new HttpHost(
573                         target.getHostName(),
574                         route.getTargetHost().getPort(),
575                         target.getSchemeName());
576             }
577             final boolean targetAuthRequested = this.authenticator.isAuthenticationRequested(
578                     target, response, this.targetAuthStrategy, targetAuthState, context);
579 
580             HttpHost proxy = route.getProxyHost();
581             // if proxy is not set use target host instead
582             if (proxy == null) {
583                 proxy = route.getTargetHost();
584             }
585             final boolean proxyAuthRequested = this.authenticator.isAuthenticationRequested(
586                     proxy, response, this.proxyAuthStrategy, proxyAuthState, context);
587 
588             if (targetAuthRequested) {
589                 return this.authenticator.handleAuthChallenge(target, response,
590                         this.targetAuthStrategy, targetAuthState, context);
591             }
592             if (proxyAuthRequested) {
593                 return this.authenticator.handleAuthChallenge(proxy, response,
594                         this.proxyAuthStrategy, proxyAuthState, context);
595             }
596         }
597         return false;
598     }
599 
600 }