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