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