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