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