001/*
002 * ====================================================================
003 * Licensed to the Apache Software Foundation (ASF) under one
004 * or more contributor license agreements.  See the NOTICE file
005 * distributed with this work for additional information
006 * regarding copyright ownership.  The ASF licenses this file
007 * to you under the Apache License, Version 2.0 (the
008 * "License"); you may not use this file except in compliance
009 * with the License.  You may obtain a copy of the License at
010 *
011 *   http://www.apache.org/licenses/LICENSE-2.0
012 *
013 * Unless required by applicable law or agreed to in writing,
014 * software distributed under the License is distributed on an
015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
016 * KIND, either express or implied.  See the License for the
017 * specific language governing permissions and limitations
018 * under the License.
019 * ====================================================================
020 *
021 * This software consists of voluntary contributions made by many
022 * individuals on behalf of the Apache Software Foundation.  For more
023 * information on the Apache Software Foundation, please see
024 * <http://www.apache.org/>.
025 *
026 */
027
028package org.apache.http.impl.execchain;
029
030import java.io.IOException;
031import java.io.InterruptedIOException;
032import java.util.concurrent.ExecutionException;
033import java.util.concurrent.TimeUnit;
034
035import org.apache.commons.logging.Log;
036import org.apache.commons.logging.LogFactory;
037import org.apache.http.ConnectionReuseStrategy;
038import org.apache.http.HttpClientConnection;
039import org.apache.http.HttpEntity;
040import org.apache.http.HttpEntityEnclosingRequest;
041import org.apache.http.HttpException;
042import org.apache.http.HttpHost;
043import org.apache.http.HttpRequest;
044import org.apache.http.HttpResponse;
045import org.apache.http.annotation.Contract;
046import org.apache.http.annotation.ThreadingBehavior;
047import org.apache.http.auth.AUTH;
048import org.apache.http.auth.AuthProtocolState;
049import org.apache.http.auth.AuthState;
050import org.apache.http.client.AuthenticationStrategy;
051import org.apache.http.client.NonRepeatableRequestException;
052import org.apache.http.client.UserTokenHandler;
053import org.apache.http.client.config.RequestConfig;
054import org.apache.http.client.methods.CloseableHttpResponse;
055import org.apache.http.client.methods.HttpExecutionAware;
056import org.apache.http.client.methods.HttpRequestWrapper;
057import org.apache.http.client.protocol.HttpClientContext;
058import org.apache.http.conn.ConnectionKeepAliveStrategy;
059import org.apache.http.conn.ConnectionRequest;
060import org.apache.http.conn.HttpClientConnectionManager;
061import org.apache.http.conn.routing.BasicRouteDirector;
062import org.apache.http.conn.routing.HttpRoute;
063import org.apache.http.conn.routing.HttpRouteDirector;
064import org.apache.http.conn.routing.RouteTracker;
065import org.apache.http.entity.BufferedHttpEntity;
066import org.apache.http.impl.auth.HttpAuthenticator;
067import org.apache.http.impl.conn.ConnectionShutdownException;
068import org.apache.http.message.BasicHttpRequest;
069import org.apache.http.protocol.HttpCoreContext;
070import org.apache.http.protocol.HttpProcessor;
071import org.apache.http.protocol.HttpRequestExecutor;
072import org.apache.http.protocol.ImmutableHttpProcessor;
073import org.apache.http.protocol.RequestTargetHost;
074import org.apache.http.util.Args;
075import org.apache.http.util.EntityUtils;
076
077/**
078 * The last request executor in the HTTP request execution chain
079 * that is responsible for execution of request / response
080 * exchanges with the opposite endpoint.
081 * This executor will automatically retry the request in case
082 * of an authentication challenge by an intermediate proxy or
083 * by the target server.
084 *
085 * @since 4.3
086 */
087@SuppressWarnings("deprecation")
088@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
089public class MainClientExec implements ClientExecChain {
090
091    private final Log log = LogFactory.getLog(getClass());
092
093    private final HttpRequestExecutor requestExecutor;
094    private final HttpClientConnectionManager connManager;
095    private final ConnectionReuseStrategy reuseStrategy;
096    private final ConnectionKeepAliveStrategy keepAliveStrategy;
097    private final HttpProcessor proxyHttpProcessor;
098    private final AuthenticationStrategy targetAuthStrategy;
099    private final AuthenticationStrategy proxyAuthStrategy;
100    private final HttpAuthenticator authenticator;
101    private final UserTokenHandler userTokenHandler;
102    private final HttpRouteDirector routeDirector;
103
104    /**
105     * @since 4.4
106     */
107    public MainClientExec(
108            final HttpRequestExecutor requestExecutor,
109            final HttpClientConnectionManager connManager,
110            final ConnectionReuseStrategy reuseStrategy,
111            final ConnectionKeepAliveStrategy keepAliveStrategy,
112            final HttpProcessor proxyHttpProcessor,
113            final AuthenticationStrategy targetAuthStrategy,
114            final AuthenticationStrategy proxyAuthStrategy,
115            final UserTokenHandler userTokenHandler) {
116        Args.notNull(requestExecutor, "HTTP request executor");
117        Args.notNull(connManager, "Client connection manager");
118        Args.notNull(reuseStrategy, "Connection reuse strategy");
119        Args.notNull(keepAliveStrategy, "Connection keep alive strategy");
120        Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
121        Args.notNull(targetAuthStrategy, "Target authentication strategy");
122        Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
123        Args.notNull(userTokenHandler, "User token handler");
124        this.authenticator      = new HttpAuthenticator();
125        this.routeDirector      = new BasicRouteDirector();
126        this.requestExecutor    = requestExecutor;
127        this.connManager        = connManager;
128        this.reuseStrategy      = reuseStrategy;
129        this.keepAliveStrategy  = keepAliveStrategy;
130        this.proxyHttpProcessor = proxyHttpProcessor;
131        this.targetAuthStrategy = targetAuthStrategy;
132        this.proxyAuthStrategy  = proxyAuthStrategy;
133        this.userTokenHandler   = userTokenHandler;
134    }
135
136    public MainClientExec(
137            final HttpRequestExecutor requestExecutor,
138            final HttpClientConnectionManager connManager,
139            final ConnectionReuseStrategy reuseStrategy,
140            final ConnectionKeepAliveStrategy keepAliveStrategy,
141            final AuthenticationStrategy targetAuthStrategy,
142            final AuthenticationStrategy proxyAuthStrategy,
143            final UserTokenHandler userTokenHandler) {
144        this(requestExecutor, connManager, reuseStrategy, keepAliveStrategy,
145                new ImmutableHttpProcessor(new RequestTargetHost()),
146                targetAuthStrategy, proxyAuthStrategy, userTokenHandler);
147    }
148
149    @Override
150    public CloseableHttpResponse execute(
151            final HttpRoute route,
152            final HttpRequestWrapper request,
153            final HttpClientContext context,
154            final HttpExecutionAware execAware) throws IOException, HttpException {
155        Args.notNull(route, "HTTP route");
156        Args.notNull(request, "HTTP request");
157        Args.notNull(context, "HTTP context");
158
159        AuthState targetAuthState = context.getTargetAuthState();
160        if (targetAuthState == null) {
161            targetAuthState = new AuthState();
162            context.setAttribute(HttpClientContext.TARGET_AUTH_STATE, targetAuthState);
163        }
164        AuthState proxyAuthState = context.getProxyAuthState();
165        if (proxyAuthState == null) {
166            proxyAuthState = new AuthState();
167            context.setAttribute(HttpClientContext.PROXY_AUTH_STATE, proxyAuthState);
168        }
169
170        if (request instanceof HttpEntityEnclosingRequest) {
171            RequestEntityProxy.enhance((HttpEntityEnclosingRequest) request);
172        }
173
174        Object userToken = context.getUserToken();
175
176        final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);
177        if (execAware != null) {
178            if (execAware.isAborted()) {
179                connRequest.cancel();
180                throw new RequestAbortedException("Request aborted");
181            } else {
182                execAware.setCancellable(connRequest);
183            }
184        }
185
186        final RequestConfig config = context.getRequestConfig();
187
188        final HttpClientConnection managedConn;
189        try {
190            final int timeout = config.getConnectionRequestTimeout();
191            managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
192        } catch(final InterruptedException interrupted) {
193            Thread.currentThread().interrupt();
194            throw new RequestAbortedException("Request aborted", interrupted);
195        } catch(final ExecutionException ex) {
196            Throwable cause = ex.getCause();
197            if (cause == null) {
198                cause = ex;
199            }
200            throw new RequestAbortedException("Request execution failed", cause);
201        }
202
203        context.setAttribute(HttpCoreContext.HTTP_CONNECTION, managedConn);
204
205        if (config.isStaleConnectionCheckEnabled()) {
206            // validate connection
207            if (managedConn.isOpen()) {
208                this.log.debug("Stale connection check");
209                if (managedConn.isStale()) {
210                    this.log.debug("Stale connection detected");
211                    managedConn.close();
212                }
213            }
214        }
215
216        final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
217        try {
218            if (execAware != null) {
219                execAware.setCancellable(connHolder);
220            }
221
222            HttpResponse response;
223            for (int execCount = 1;; execCount++) {
224
225                if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) {
226                    throw new NonRepeatableRequestException("Cannot retry request " +
227                            "with a non-repeatable request entity.");
228                }
229
230                if (execAware != null && execAware.isAborted()) {
231                    throw new RequestAbortedException("Request aborted");
232                }
233
234                if (!managedConn.isOpen()) {
235                    this.log.debug("Opening connection " + route);
236                    try {
237                        establishRoute(proxyAuthState, managedConn, route, request, context);
238                    } catch (final TunnelRefusedException ex) {
239                        if (this.log.isDebugEnabled()) {
240                            this.log.debug(ex.getMessage());
241                        }
242                        response = ex.getResponse();
243                        break;
244                    }
245                }
246                final int timeout = config.getSocketTimeout();
247                if (timeout >= 0) {
248                    managedConn.setSocketTimeout(timeout);
249                }
250
251                if (execAware != null && execAware.isAborted()) {
252                    throw new RequestAbortedException("Request aborted");
253                }
254
255                if (this.log.isDebugEnabled()) {
256                    this.log.debug("Executing request " + request.getRequestLine());
257                }
258
259                if (!request.containsHeader(AUTH.WWW_AUTH_RESP)) {
260                    if (this.log.isDebugEnabled()) {
261                        this.log.debug("Target auth state: " + targetAuthState.getState());
262                    }
263                    this.authenticator.generateAuthResponse(request, targetAuthState, context);
264                }
265                if (!request.containsHeader(AUTH.PROXY_AUTH_RESP) && !route.isTunnelled()) {
266                    if (this.log.isDebugEnabled()) {
267                        this.log.debug("Proxy auth state: " + proxyAuthState.getState());
268                    }
269                    this.authenticator.generateAuthResponse(request, proxyAuthState, context);
270                }
271
272                response = requestExecutor.execute(request, managedConn, context);
273
274                // The connection is in or can be brought to a re-usable state.
275                if (reuseStrategy.keepAlive(response, context)) {
276                    // Set the idle duration of this connection
277                    final long duration = keepAliveStrategy.getKeepAliveDuration(response, context);
278                    if (this.log.isDebugEnabled()) {
279                        final String s;
280                        if (duration > 0) {
281                            s = "for " + duration + " " + TimeUnit.MILLISECONDS;
282                        } else {
283                            s = "indefinitely";
284                        }
285                        this.log.debug("Connection can be kept alive " + s);
286                    }
287                    connHolder.setValidFor(duration, TimeUnit.MILLISECONDS);
288                    connHolder.markReusable();
289                } else {
290                    connHolder.markNonReusable();
291                }
292
293                if (needAuthentication(
294                        targetAuthState, proxyAuthState, route, response, context)) {
295                    // Make sure the response body is fully consumed, if present
296                    final HttpEntity entity = response.getEntity();
297                    if (connHolder.isReusable()) {
298                        EntityUtils.consume(entity);
299                    } else {
300                        managedConn.close();
301                        if (proxyAuthState.getState() == AuthProtocolState.SUCCESS
302                                && proxyAuthState.getAuthScheme() != null
303                                && proxyAuthState.getAuthScheme().isConnectionBased()) {
304                            this.log.debug("Resetting proxy auth state");
305                            proxyAuthState.reset();
306                        }
307                        if (targetAuthState.getState() == AuthProtocolState.SUCCESS
308                                && targetAuthState.getAuthScheme() != null
309                                && targetAuthState.getAuthScheme().isConnectionBased()) {
310                            this.log.debug("Resetting target auth state");
311                            targetAuthState.reset();
312                        }
313                    }
314                    // discard previous auth headers
315                    final HttpRequest original = request.getOriginal();
316                    if (!original.containsHeader(AUTH.WWW_AUTH_RESP)) {
317                        request.removeHeaders(AUTH.WWW_AUTH_RESP);
318                    }
319                    if (!original.containsHeader(AUTH.PROXY_AUTH_RESP)) {
320                        request.removeHeaders(AUTH.PROXY_AUTH_RESP);
321                    }
322                } else {
323                    break;
324                }
325            }
326
327            if (userToken == null) {
328                userToken = userTokenHandler.getUserToken(context);
329                context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
330            }
331            if (userToken != null) {
332                connHolder.setState(userToken);
333            }
334
335            // check for entity, release connection if possible
336            final HttpEntity entity = response.getEntity();
337            if (entity == null || !entity.isStreaming()) {
338                // connection not needed and (assumed to be) in re-usable state
339                connHolder.releaseConnection();
340                return new HttpResponseProxy(response, null);
341            } else {
342                return new HttpResponseProxy(response, connHolder);
343            }
344        } catch (final ConnectionShutdownException ex) {
345            final InterruptedIOException ioex = new InterruptedIOException(
346                    "Connection has been shut down");
347            ioex.initCause(ex);
348            throw ioex;
349        } catch (final HttpException ex) {
350            connHolder.abortConnection();
351            throw ex;
352        } catch (final IOException ex) {
353            connHolder.abortConnection();
354            throw ex;
355        } catch (final RuntimeException ex) {
356            connHolder.abortConnection();
357            throw ex;
358        }
359    }
360
361    /**
362     * Establishes the target route.
363     */
364    void establishRoute(
365            final AuthState proxyAuthState,
366            final HttpClientConnection managedConn,
367            final HttpRoute route,
368            final HttpRequest request,
369            final HttpClientContext context) throws HttpException, IOException {
370        final RequestConfig config = context.getRequestConfig();
371        final int timeout = config.getConnectTimeout();
372        final RouteTracker tracker = new RouteTracker(route);
373        int step;
374        do {
375            final HttpRoute fact = tracker.toRoute();
376            step = this.routeDirector.nextStep(route, fact);
377
378            switch (step) {
379
380            case HttpRouteDirector.CONNECT_TARGET:
381                this.connManager.connect(
382                        managedConn,
383                        route,
384                        timeout > 0 ? timeout : 0,
385                        context);
386                tracker.connectTarget(route.isSecure());
387                break;
388            case HttpRouteDirector.CONNECT_PROXY:
389                this.connManager.connect(
390                        managedConn,
391                        route,
392                        timeout > 0 ? timeout : 0,
393                        context);
394                final HttpHost proxy  = route.getProxyHost();
395                tracker.connectProxy(proxy, false);
396                break;
397            case HttpRouteDirector.TUNNEL_TARGET: {
398                final boolean secure = createTunnelToTarget(
399                        proxyAuthState, managedConn, route, request, context);
400                this.log.debug("Tunnel to target created.");
401                tracker.tunnelTarget(secure);
402            }   break;
403
404            case HttpRouteDirector.TUNNEL_PROXY: {
405                // The most simple example for this case is a proxy chain
406                // of two proxies, where P1 must be tunnelled to P2.
407                // route: Source -> P1 -> P2 -> Target (3 hops)
408                // fact:  Source -> P1 -> Target       (2 hops)
409                final int hop = fact.getHopCount()-1; // the hop to establish
410                final boolean secure = createTunnelToProxy(route, hop, context);
411                this.log.debug("Tunnel to proxy created.");
412                tracker.tunnelProxy(route.getHopTarget(hop), secure);
413            }   break;
414
415            case HttpRouteDirector.LAYER_PROTOCOL:
416                this.connManager.upgrade(managedConn, route, context);
417                tracker.layerProtocol(route.isSecure());
418                break;
419
420            case HttpRouteDirector.UNREACHABLE:
421                throw new HttpException("Unable to establish route: " +
422                        "planned = " + route + "; current = " + fact);
423            case HttpRouteDirector.COMPLETE:
424                this.connManager.routeComplete(managedConn, route, context);
425                break;
426            default:
427                throw new IllegalStateException("Unknown step indicator "
428                        + step + " from RouteDirector.");
429            }
430
431        } while (step > HttpRouteDirector.COMPLETE);
432    }
433
434    /**
435     * Creates a tunnel to the target server.
436     * The connection must be established to the (last) proxy.
437     * A CONNECT request for tunnelling through the proxy will
438     * be created and sent, the response received and checked.
439     * This method does <i>not</i> update the connection with
440     * information about the tunnel, that is left to the caller.
441     */
442    private boolean createTunnelToTarget(
443            final AuthState proxyAuthState,
444            final HttpClientConnection managedConn,
445            final HttpRoute route,
446            final HttpRequest request,
447            final HttpClientContext context) throws HttpException, IOException {
448
449        final RequestConfig config = context.getRequestConfig();
450        final int timeout = config.getConnectTimeout();
451
452        final HttpHost target = route.getTargetHost();
453        final HttpHost proxy = route.getProxyHost();
454        HttpResponse response = null;
455
456        final String authority = target.toHostString();
457        final HttpRequest connect = new BasicHttpRequest("CONNECT", authority, request.getProtocolVersion());
458
459        this.requestExecutor.preProcess(connect, this.proxyHttpProcessor, context);
460
461        while (response == null) {
462            if (!managedConn.isOpen()) {
463                this.connManager.connect(
464                        managedConn,
465                        route,
466                        timeout > 0 ? timeout : 0,
467                        context);
468            }
469
470            connect.removeHeaders(AUTH.PROXY_AUTH_RESP);
471            this.authenticator.generateAuthResponse(connect, proxyAuthState, context);
472
473            response = this.requestExecutor.execute(connect, managedConn, context);
474
475            final int status = response.getStatusLine().getStatusCode();
476            if (status < 200) {
477                throw new HttpException("Unexpected response to CONNECT request: " +
478                        response.getStatusLine());
479            }
480
481            if (config.isAuthenticationEnabled()) {
482                if (this.authenticator.isAuthenticationRequested(proxy, response,
483                        this.proxyAuthStrategy, proxyAuthState, context)) {
484                    if (this.authenticator.handleAuthChallenge(proxy, response,
485                            this.proxyAuthStrategy, proxyAuthState, context)) {
486                        // Retry request
487                        if (this.reuseStrategy.keepAlive(response, context)) {
488                            this.log.debug("Connection kept alive");
489                            // Consume response content
490                            final HttpEntity entity = response.getEntity();
491                            EntityUtils.consume(entity);
492                        } else {
493                            managedConn.close();
494                        }
495                        response = null;
496                    }
497                }
498            }
499        }
500
501        final int status = response.getStatusLine().getStatusCode();
502
503        if (status > 299) {
504
505            // Buffer response content
506            final HttpEntity entity = response.getEntity();
507            if (entity != null) {
508                response.setEntity(new BufferedHttpEntity(entity));
509            }
510
511            managedConn.close();
512            throw new TunnelRefusedException("CONNECT refused by proxy: " +
513                    response.getStatusLine(), response);
514        }
515
516        // How to decide on security of the tunnelled connection?
517        // The socket factory knows only about the segment to the proxy.
518        // Even if that is secure, the hop to the target may be insecure.
519        // Leave it to derived classes, consider insecure by default here.
520        return false;
521    }
522
523    /**
524     * Creates a tunnel to an intermediate proxy.
525     * This method is <i>not</i> implemented in this class.
526     * It just throws an exception here.
527     */
528    private boolean createTunnelToProxy(
529            final HttpRoute route,
530            final int hop,
531            final HttpClientContext context) throws HttpException {
532
533        // Have a look at createTunnelToTarget and replicate the parts
534        // you need in a custom derived class. If your proxies don't require
535        // authentication, it is not too hard. But for the stock version of
536        // HttpClient, we cannot make such simplifying assumptions and would
537        // have to include proxy authentication code. The HttpComponents team
538        // is currently not in a position to support rarely used code of this
539        // complexity. Feel free to submit patches that refactor the code in
540        // createTunnelToTarget to facilitate re-use for proxy tunnelling.
541
542        throw new HttpException("Proxy chains are not supported.");
543    }
544
545    private boolean needAuthentication(
546            final AuthState targetAuthState,
547            final AuthState proxyAuthState,
548            final HttpRoute route,
549            final HttpResponse response,
550            final HttpClientContext context) {
551        final RequestConfig config = context.getRequestConfig();
552        if (config.isAuthenticationEnabled()) {
553            HttpHost target = context.getTargetHost();
554            if (target == null) {
555                target = route.getTargetHost();
556            }
557            if (target.getPort() < 0) {
558                target = new HttpHost(
559                        target.getHostName(),
560                        route.getTargetHost().getPort(),
561                        target.getSchemeName());
562            }
563            final boolean targetAuthRequested = this.authenticator.isAuthenticationRequested(
564                    target, response, this.targetAuthStrategy, targetAuthState, context);
565
566            HttpHost proxy = route.getProxyHost();
567            // if proxy is not set use target host instead
568            if (proxy == null) {
569                proxy = route.getTargetHost();
570            }
571            final boolean proxyAuthRequested = this.authenticator.isAuthenticationRequested(
572                    proxy, response, this.proxyAuthStrategy, proxyAuthState, context);
573
574            if (targetAuthRequested) {
575                return this.authenticator.handleAuthChallenge(target, response,
576                        this.targetAuthStrategy, targetAuthState, context);
577            }
578            if (proxyAuthRequested) {
579                return this.authenticator.handleAuthChallenge(proxy, response,
580                        this.proxyAuthStrategy, proxyAuthState, context);
581            }
582        }
583        return false;
584    }
585
586}