You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2017/05/01 13:06:48 UTC

svn commit: r1793325 [1/2] - in /httpcomponents/httpclient/trunk/httpclient5/src: main/java/org/apache/hc/client5/http/impl/sync/ test/java/org/apache/hc/client5/http/impl/sync/

Author: olegk
Date: Mon May  1 13:06:48 2017
New Revision: 1793325

URL: http://svn.apache.org/viewvc?rev=1793325&view=rev
Log:
Refactored connection routing and protocol execution code in the classic exec chain

Added:
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java   (with props)
    httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java   (with props)
Removed:
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MinimalClientExec.java
    httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestMinimalClientExec.java
Modified:
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ChainElements.java
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MainClientExec.java
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MinimalHttpClient.java
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ProtocolExec.java
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RequestEntityProxy.java
    httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RetryExec.java
    httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestMainClientExec.java
    httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestProtocolExec.java

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ChainElements.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ChainElements.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ChainElements.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ChainElements.java Mon May  1 13:06:48 2017
@@ -29,6 +29,6 @@ package org.apache.hc.client5.http.impl.
 
 public enum ChainElements {
 
-    REDIRECT, BACK_OFF, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, PROTOCOL, MAIN_TRANSPORT
+    REDIRECT, BACK_OFF, RETRY_SERVICE_UNAVAILABLE, RETRY_IO_ERROR, PROTOCOL, CONNECT, MAIN_TRANSPORT
 
 }
\ No newline at end of file

Added: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java?rev=1793325&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java (added)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java Mon May  1 13:06:48 2017
@@ -0,0 +1,282 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+
+package org.apache.hc.client5.http.impl.sync;
+
+import java.io.IOException;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.RouteTracker;
+import org.apache.hc.client5.http.auth.AuthExchange;
+import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
+import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
+import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.routing.HttpRouteDirector;
+import org.apache.hc.client5.http.sync.ExecChain;
+import org.apache.hc.client5.http.sync.ExecChainHandler;
+import org.apache.hc.client5.http.sync.ExecRuntime;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ConnectionReuseStrategy;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.http.message.StatusLine;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.util.Args;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+/**
+ * Request executor in the HTTP request execution chain
+ * that is responsible for establishing connection to the target
+ * origin server as specified by the current route.
+ *
+ * @since 5.0
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL)
+public final class ConnectExec implements ExecChainHandler {
+
+    private final Logger log = LogManager.getLogger(getClass());
+
+    private final ConnectionReuseStrategy reuseStrategy;
+    private final HttpProcessor proxyHttpProcessor;
+    private final AuthenticationStrategy proxyAuthStrategy;
+    private final HttpAuthenticator authenticator;
+    private final HttpRouteDirector routeDirector;
+
+    public ConnectExec(
+            final ConnectionReuseStrategy reuseStrategy,
+            final HttpProcessor proxyHttpProcessor,
+            final AuthenticationStrategy proxyAuthStrategy) {
+        Args.notNull(reuseStrategy, "Connection reuse strategy");
+        Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
+        Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
+        this.reuseStrategy      = reuseStrategy;
+        this.proxyHttpProcessor = proxyHttpProcessor;
+        this.proxyAuthStrategy  = proxyAuthStrategy;
+        this.authenticator      = new HttpAuthenticator();
+        this.routeDirector      = new BasicRouteDirector();
+    }
+
+    @Override
+    public ClassicHttpResponse execute(
+            final ClassicHttpRequest request,
+            final ExecChain.Scope scope,
+            final ExecChain chain) throws IOException, HttpException {
+        Args.notNull(request, "HTTP request");
+        Args.notNull(scope, "Scope");
+
+        final HttpRoute route = scope.route;
+        final HttpClientContext context = scope.clientContext;
+        final ExecRuntime execRuntime = scope.execRuntime;
+
+        if (!execRuntime.isConnectionAcquired()) {
+            final Object userToken = context.getUserToken();
+            execRuntime.acquireConnection(route, userToken, context);
+        }
+        try {
+            if (!execRuntime.isConnected()) {
+                this.log.debug("Opening connection " + route);
+
+                final RouteTracker tracker = new RouteTracker(route);
+                int step;
+                do {
+                    final HttpRoute fact = tracker.toRoute();
+                    step = this.routeDirector.nextStep(route, fact);
+
+                    switch (step) {
+
+                        case HttpRouteDirector.CONNECT_TARGET:
+                            execRuntime.connect(context);
+                            tracker.connectTarget(route.isSecure());
+                            break;
+                        case HttpRouteDirector.CONNECT_PROXY:
+                            execRuntime.connect(context);
+                            final HttpHost proxy  = route.getProxyHost();
+                            tracker.connectProxy(proxy, false);
+                            break;
+                        case HttpRouteDirector.TUNNEL_TARGET: {
+                            final boolean secure = createTunnelToTarget(route, request, execRuntime, context);
+                            this.log.debug("Tunnel to target created.");
+                            tracker.tunnelTarget(secure);
+                        }   break;
+
+                        case HttpRouteDirector.TUNNEL_PROXY: {
+                            // The most simple example for this case is a proxy chain
+                            // of two proxies, where P1 must be tunnelled to P2.
+                            // route: Source -> P1 -> P2 -> Target (3 hops)
+                            // fact:  Source -> P1 -> Target       (2 hops)
+                            final int hop = fact.getHopCount()-1; // the hop to establish
+                            final boolean secure = createTunnelToProxy(route, hop, context);
+                            this.log.debug("Tunnel to proxy created.");
+                            tracker.tunnelProxy(route.getHopTarget(hop), secure);
+                        }   break;
+
+                        case HttpRouteDirector.LAYER_PROTOCOL:
+                            execRuntime.upgradeTls(context);
+                            tracker.layerProtocol(route.isSecure());
+                            break;
+
+                        case HttpRouteDirector.UNREACHABLE:
+                            throw new HttpException("Unable to establish route: " +
+                                    "planned = " + route + "; current = " + fact);
+                        case HttpRouteDirector.COMPLETE:
+                            break;
+                        default:
+                            throw new IllegalStateException("Unknown step indicator "
+                                    + step + " from RouteDirector.");
+                    }
+
+                } while (step > HttpRouteDirector.COMPLETE);
+            }
+            return chain.proceed(request, scope);
+
+        } catch (final IOException | HttpException | RuntimeException ex) {
+            execRuntime.discardConnection();
+            throw ex;
+        }
+    }
+
+    /**
+     * Creates a tunnel to the target server.
+     * The connection must be established to the (last) proxy.
+     * A CONNECT request for tunnelling through the proxy will
+     * be created and sent, the response received and checked.
+     * This method does <i>not</i> processChallenge the connection with
+     * information about the tunnel, that is left to the caller.
+     */
+    private boolean createTunnelToTarget(
+            final HttpRoute route,
+            final HttpRequest request,
+            final ExecRuntime execRuntime,
+            final HttpClientContext context) throws HttpException, IOException {
+
+        final RequestConfig config = context.getRequestConfig();
+
+        final HttpHost target = route.getTargetHost();
+        final HttpHost proxy = route.getProxyHost();
+        final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
+        ClassicHttpResponse response = null;
+
+        final String authority = target.toHostString();
+        final ClassicHttpRequest connect = new BasicClassicHttpRequest("CONNECT", target, authority);
+        connect.setVersion(HttpVersion.HTTP_1_1);
+
+        this.proxyHttpProcessor.process(connect, null, context);
+
+        while (response == null) {
+            if (!execRuntime.isConnected()) {
+                execRuntime.connect(context);
+            }
+
+            connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
+            this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
+
+            response = execRuntime.execute(connect, context);
+
+            final int status = response.getCode();
+            if (status < HttpStatus.SC_SUCCESS) {
+                throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
+            }
+
+            if (config.isAuthenticationEnabled()) {
+                if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response,
+                        proxyAuthExchange, context)) {
+                    if (this.authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
+                            this.proxyAuthStrategy, proxyAuthExchange, context)) {
+                        // Retry request
+                        if (this.reuseStrategy.keepAlive(request, response, context)) {
+                            this.log.debug("Connection kept alive");
+                            // Consume response content
+                            final HttpEntity entity = response.getEntity();
+                            EntityUtils.consume(entity);
+                        } else {
+                            execRuntime.disconnect();
+                        }
+                        response = null;
+                    }
+                }
+            }
+        }
+
+        final int status = response.getCode();
+        if (status >= HttpStatus.SC_REDIRECTION) {
+
+            // Buffer response content
+            final HttpEntity entity = response.getEntity();
+            if (entity != null) {
+                response.setEntity(new BufferedHttpEntity(entity));
+            }
+
+            execRuntime.disconnect();
+            throw new TunnelRefusedException("CONNECT refused by proxy: " +
+                    new StatusLine(response), response);
+        }
+
+        // How to decide on security of the tunnelled connection?
+        // The socket factory knows only about the segment to the proxy.
+        // Even if that is secure, the hop to the target may be insecure.
+        // Leave it to derived classes, consider insecure by default here.
+        return false;
+    }
+
+    /**
+     * Creates a tunnel to an intermediate proxy.
+     * This method is <i>not</i> implemented in this class.
+     * It just throws an exception here.
+     */
+    private boolean createTunnelToProxy(
+            final HttpRoute route,
+            final int hop,
+            final HttpClientContext context) throws HttpException {
+
+        // Have a look at createTunnelToTarget and replicate the parts
+        // you need in a custom derived class. If your proxies don't require
+        // authentication, it is not too hard. But for the stock version of
+        // HttpClient, we cannot make such simplifying assumptions and would
+        // have to include proxy authentication code. The HttpComponents team
+        // is currently not in a position to support rarely used code of this
+        // complexity. Feel free to submit patches that refactor the code in
+        // createTunnelToTarget to facilitate re-use for proxy tunnelling.
+
+        throw new HttpException("Proxy chains are not supported.");
+    }
+
+}

Propchange: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java
------------------------------------------------------------------------------
    svn:keywords = Date Revision

Propchange: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ConnectExec.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/HttpClientBuilder.java Mon May  1 13:06:48 2017
@@ -99,6 +99,7 @@ import org.apache.hc.core5.http.impl.Def
 import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
 import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
 import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
 import org.apache.hc.core5.http.protocol.HttpProcessorBuilder;
 import org.apache.hc.core5.http.protocol.RequestContent;
 import org.apache.hc.core5.http.protocol.RequestTargetHost;
@@ -725,6 +726,7 @@ public class HttpClientBuilder {
                 reuseStrategyCopy = DefaultConnectionReuseStrategy.INSTANCE;
             }
         }
+
         ConnectionKeepAliveStrategy keepAliveStrategyCopy = this.keepAliveStrategy;
         if (keepAliveStrategyCopy == null) {
             keepAliveStrategyCopy = DefaultConnectionKeepAliveStrategy.INSTANCE;
@@ -759,14 +761,14 @@ public class HttpClientBuilder {
 
         final NamedElementChain<ExecChainHandler> execChainDefinition = new NamedElementChain<>();
         execChainDefinition.addLast(
-                new MainClientExec(
+                new MainClientExec(reuseStrategyCopy, keepAliveStrategyCopy, userTokenHandlerCopy),
+                ChainElements.MAIN_TRANSPORT.name());
+        execChainDefinition.addFirst(
+                new ConnectExec(
                         reuseStrategyCopy,
-                        keepAliveStrategyCopy,
                         new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)),
-                        targetAuthStrategyCopy,
-                        proxyAuthStrategyCopy,
-                        userTokenHandlerCopy),
-                ChainElements.MAIN_TRANSPORT.name());
+                        proxyAuthStrategyCopy),
+                ChainElements.CONNECT.name());
 
         final HttpProcessorBuilder b = HttpProcessorBuilder.create();
         if (requestInterceptors != null) {
@@ -813,8 +815,9 @@ public class HttpClientBuilder {
                 }
             }
         }
+        final HttpProcessor httpProcessor = b.build();
         execChainDefinition.addFirst(
-                new ProtocolExec(b.build()),
+                new ProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy),
                 ChainElements.PROTOCOL.name());
 
         // Add request retry executor, if not disabled

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MainClientExec.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MainClientExec.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MainClientExec.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MainClientExec.java Mon May  1 13:06:48 2017
@@ -32,18 +32,9 @@ import java.io.InterruptedIOException;
 
 import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
 import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.RouteTracker;
-import org.apache.hc.client5.http.auth.AuthExchange;
-import org.apache.hc.client5.http.auth.ChallengeType;
-import org.apache.hc.client5.http.config.RequestConfig;
 import org.apache.hc.client5.http.impl.ConnectionShutdownException;
-import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
-import org.apache.hc.client5.http.impl.routing.BasicRouteDirector;
-import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.protocol.NonRepeatableRequestException;
 import org.apache.hc.client5.http.protocol.UserTokenHandler;
-import org.apache.hc.client5.http.routing.HttpRouteDirector;
 import org.apache.hc.client5.http.sync.ExecChain;
 import org.apache.hc.client5.http.sync.ExecChainHandler;
 import org.apache.hc.client5.http.sync.ExecRuntime;
@@ -54,21 +45,7 @@ import org.apache.hc.core5.http.ClassicH
 import org.apache.hc.core5.http.ConnectionReuseStrategy;
 import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
-import org.apache.hc.core5.http.HttpHeaders;
-import org.apache.hc.core5.http.HttpHost;
-import org.apache.hc.core5.http.HttpRequest;
-import org.apache.hc.core5.http.HttpResponse;
-import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.HttpVersion;
-import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
-import org.apache.hc.core5.http.io.entity.EntityUtils;
-import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
 import org.apache.hc.core5.http.message.RequestLine;
-import org.apache.hc.core5.http.message.StatusLine;
-import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
-import org.apache.hc.core5.http.protocol.HttpProcessor;
-import org.apache.hc.core5.http.protocol.RequestTargetHost;
-import org.apache.hc.core5.net.URIAuthority;
 import org.apache.hc.core5.util.Args;
 import org.apache.hc.core5.util.TimeValue;
 import org.apache.logging.log4j.LogManager;
@@ -78,9 +55,6 @@ import org.apache.logging.log4j.Logger;
  * The last request executor in the HTTP request execution chain
  * that is responsible for execution of request / response
  * exchanges with the opposite endpoint.
- * This executor will automatically retry the request in case
- * of an authentication challenge by an intermediate proxy or
- * by the target server.
  *
  * @since 4.3
  */
@@ -91,12 +65,7 @@ final class MainClientExec implements Ex
 
     private final ConnectionReuseStrategy reuseStrategy;
     private final ConnectionKeepAliveStrategy keepAliveStrategy;
-    private final HttpProcessor proxyHttpProcessor;
-    private final AuthenticationStrategy targetAuthStrategy;
-    private final AuthenticationStrategy proxyAuthStrategy;
-    private final HttpAuthenticator authenticator;
     private final UserTokenHandler userTokenHandler;
-    private final HttpRouteDirector routeDirector;
 
     /**
      * @since 4.4
@@ -104,37 +73,15 @@ final class MainClientExec implements Ex
     public MainClientExec(
             final ConnectionReuseStrategy reuseStrategy,
             final ConnectionKeepAliveStrategy keepAliveStrategy,
-            final HttpProcessor proxyHttpProcessor,
-            final AuthenticationStrategy targetAuthStrategy,
-            final AuthenticationStrategy proxyAuthStrategy,
             final UserTokenHandler userTokenHandler) {
         Args.notNull(reuseStrategy, "Connection reuse strategy");
         Args.notNull(keepAliveStrategy, "Connection keep alive strategy");
-        Args.notNull(proxyHttpProcessor, "Proxy HTTP processor");
-        Args.notNull(targetAuthStrategy, "Target authentication strategy");
-        Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
         Args.notNull(userTokenHandler, "User token handler");
-        this.authenticator      = new HttpAuthenticator();
-        this.routeDirector      = new BasicRouteDirector();
         this.reuseStrategy      = reuseStrategy;
         this.keepAliveStrategy  = keepAliveStrategy;
-        this.proxyHttpProcessor = proxyHttpProcessor;
-        this.targetAuthStrategy = targetAuthStrategy;
-        this.proxyAuthStrategy  = proxyAuthStrategy;
         this.userTokenHandler   = userTokenHandler;
     }
 
-    public MainClientExec(
-            final ConnectionReuseStrategy reuseStrategy,
-            final ConnectionKeepAliveStrategy keepAliveStrategy,
-            final AuthenticationStrategy targetAuthStrategy,
-            final AuthenticationStrategy proxyAuthStrategy,
-            final UserTokenHandler userTokenHandler) {
-        this(reuseStrategy, keepAliveStrategy,
-                new DefaultHttpProcessor(new RequestTargetHost()),
-                targetAuthStrategy, proxyAuthStrategy, userTokenHandler);
-    }
-
     @Override
     public ClassicHttpResponse execute(
             final ClassicHttpRequest request,
@@ -146,116 +93,15 @@ final class MainClientExec implements Ex
         final HttpClientContext context = scope.clientContext;
         final ExecRuntime execRuntime = scope.execRuntime;
 
-        RequestEntityProxy.enhance(request);
-
-        Object userToken = context.getUserToken();
-        if (!execRuntime.isConnectionAcquired()) {
-            execRuntime.acquireConnection(route, userToken, context);
-        }
         try {
-            final AuthExchange targetAuthExchange = context.getAuthExchange(route.getTargetHost());
-            final AuthExchange proxyAuthExchange = route.getProxyHost() != null ?
-                    context.getAuthExchange(route.getProxyHost()) : new AuthExchange();
-
-            ClassicHttpResponse response;
-            for (int execCount = 1;; execCount++) {
-
-                if (execCount > 1 && !RequestEntityProxy.isRepeatable(request)) {
-                    throw new NonRepeatableRequestException("Cannot retry request " +
-                            "with a non-repeatable request entity.");
-                }
-
-                if (!execRuntime.isConnected()) {
-                    this.log.debug("Opening connection " + route);
-                    try {
-                        establishRoute(route, request, execRuntime, context);
-                    } catch (final TunnelRefusedException ex) {
-                        if (this.log.isDebugEnabled()) {
-                            this.log.debug(ex.getMessage());
-                        }
-                        response = ex.getResponse();
-                        break;
-                    }
-                }
-                if (this.log.isDebugEnabled()) {
-                    this.log.debug("Executing request " + new RequestLine(request));
-                }
-
-                if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
-                    if (this.log.isDebugEnabled()) {
-                        this.log.debug("Target auth state: " + targetAuthExchange.getState());
-                    }
-                    this.authenticator.addAuthResponse(
-                            route.getTargetHost(), ChallengeType.TARGET, request, targetAuthExchange, context);
-                }
-                if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
-                    if (this.log.isDebugEnabled()) {
-                        this.log.debug("Proxy auth state: " + proxyAuthExchange.getState());
-                    }
-                    this.authenticator.addAuthResponse(
-                            route.getProxyHost(), ChallengeType.PROXY, request, proxyAuthExchange, context);
-                }
-
-                response = execRuntime.execute(request, context);
-
-                // The connection is in or can be brought to a re-usable state.
-                if (reuseStrategy.keepAlive(request, response, context)) {
-                    // Set the idle duration of this connection
-                    final TimeValue duration = keepAliveStrategy.getKeepAliveDuration(response, context);
-                    if (this.log.isDebugEnabled()) {
-                        final String s;
-                        if (duration != null) {
-                            s = "for " + duration;
-                        } else {
-                            s = "indefinitely";
-                        }
-                        this.log.debug("Connection can be kept alive " + s);
-                    }
-                    execRuntime.setConnectionValidFor(duration);
-                    execRuntime.markConnectionReusable();
-                } else {
-                    execRuntime.markConnectionNonReusable();
-                }
-
-                if (request.getMethod().equalsIgnoreCase("TRACE")) {
-                    // Do not perform authentication for TRACE request
-                    break;
-                }
-
-                if (needAuthentication(
-                        targetAuthExchange, proxyAuthExchange, route, request, response, context)) {
-                    // Make sure the response body is fully consumed, if present
-                    final HttpEntity entity = response.getEntity();
-                    if (execRuntime.isConnectionReusable()) {
-                        EntityUtils.consume(entity);
-                    } else {
-                        execRuntime.disconnect();
-                        if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
-                                && proxyAuthExchange.getAuthScheme() != null
-                                && proxyAuthExchange.getAuthScheme().isConnectionBased()) {
-                            this.log.debug("Resetting proxy auth state");
-                            proxyAuthExchange.reset();
-                        }
-                        if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
-                                && targetAuthExchange.getAuthScheme() != null
-                                && targetAuthExchange.getAuthScheme().isConnectionBased()) {
-                            this.log.debug("Resetting target auth state");
-                            targetAuthExchange.reset();
-                        }
-                    }
-                    // discard previous auth headers
-                    final HttpRequest original = scope.originalRequest;
-                    if (!original.containsHeader(HttpHeaders.AUTHORIZATION)) {
-                        request.removeHeaders(HttpHeaders.AUTHORIZATION);
-                    }
-                    if (!original.containsHeader(HttpHeaders.PROXY_AUTHORIZATION)) {
-                        request.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
-                    }
-                } else {
-                    break;
-                }
+            if (this.log.isDebugEnabled()) {
+                this.log.debug("Executing request " + new RequestLine(request));
             }
+            RequestEntityProxy.enhance(request);
+
+            final ClassicHttpResponse response = execRuntime.execute(request, context);
 
+            Object userToken = context.getUserToken();
             if (userToken == null) {
                 userToken = userTokenHandler.getUserToken(route, context);
                 context.setAttribute(HttpClientContext.USER_TOKEN, userToken);
@@ -264,6 +110,24 @@ final class MainClientExec implements Ex
                 execRuntime.setConnectionState(userToken);
             }
 
+            // The connection is in or can be brought to a re-usable state.
+            if (reuseStrategy.keepAlive(request, response, context)) {
+                // Set the idle duration of this connection
+                final TimeValue duration = keepAliveStrategy.getKeepAliveDuration(response, context);
+                if (this.log.isDebugEnabled()) {
+                    final String s;
+                    if (duration != null) {
+                        s = "for " + duration;
+                    } else {
+                        s = "indefinitely";
+                    }
+                    this.log.debug("Connection can be kept alive " + s);
+                }
+                execRuntime.setConnectionValidFor(duration);
+                execRuntime.markConnectionReusable();
+            } else {
+                execRuntime.markConnectionNonReusable();
+            }
             // check for entity, release connection if possible
             final HttpEntity entity = response.getEntity();
             if (entity == null || !entity.isStreaming()) {
@@ -284,213 +148,7 @@ final class MainClientExec implements Ex
             execRuntime.discardConnection();
             throw ex;
         }
-    }
-
-    /**
-     * Establishes the target route.
-     */
-    void establishRoute(
-            final HttpRoute route,
-            final HttpRequest request,
-            final ExecRuntime execRuntime,
-            final HttpClientContext context) throws HttpException, IOException {
-        final RouteTracker tracker = new RouteTracker(route);
-        int step;
-        do {
-            final HttpRoute fact = tracker.toRoute();
-            step = this.routeDirector.nextStep(route, fact);
-
-            switch (step) {
-
-            case HttpRouteDirector.CONNECT_TARGET:
-                execRuntime.connect(context);
-                tracker.connectTarget(route.isSecure());
-                break;
-            case HttpRouteDirector.CONNECT_PROXY:
-                execRuntime.connect(context);
-                final HttpHost proxy  = route.getProxyHost();
-                tracker.connectProxy(proxy, false);
-                break;
-            case HttpRouteDirector.TUNNEL_TARGET: {
-                final boolean secure = createTunnelToTarget(route, request, execRuntime, context);
-                this.log.debug("Tunnel to target created.");
-                tracker.tunnelTarget(secure);
-            }   break;
-
-            case HttpRouteDirector.TUNNEL_PROXY: {
-                // The most simple example for this case is a proxy chain
-                // of two proxies, where P1 must be tunnelled to P2.
-                // route: Source -> P1 -> P2 -> Target (3 hops)
-                // fact:  Source -> P1 -> Target       (2 hops)
-                final int hop = fact.getHopCount()-1; // the hop to establish
-                final boolean secure = createTunnelToProxy(route, hop, context);
-                this.log.debug("Tunnel to proxy created.");
-                tracker.tunnelProxy(route.getHopTarget(hop), secure);
-            }   break;
-
-            case HttpRouteDirector.LAYER_PROTOCOL:
-                execRuntime.upgradeTls(context);
-                tracker.layerProtocol(route.isSecure());
-                break;
-
-            case HttpRouteDirector.UNREACHABLE:
-                throw new HttpException("Unable to establish route: " +
-                        "planned = " + route + "; current = " + fact);
-            case HttpRouteDirector.COMPLETE:
-                break;
-            default:
-                throw new IllegalStateException("Unknown step indicator "
-                        + step + " from RouteDirector.");
-            }
-
-        } while (step > HttpRouteDirector.COMPLETE);
-    }
-
-    /**
-     * Creates a tunnel to the target server.
-     * The connection must be established to the (last) proxy.
-     * A CONNECT request for tunnelling through the proxy will
-     * be created and sent, the response received and checked.
-     * This method does <i>not</i> processChallenge the connection with
-     * information about the tunnel, that is left to the caller.
-     */
-    private boolean createTunnelToTarget(
-            final HttpRoute route,
-            final HttpRequest request,
-            final ExecRuntime execRuntime,
-            final HttpClientContext context) throws HttpException, IOException {
-
-        final RequestConfig config = context.getRequestConfig();
-
-        final HttpHost target = route.getTargetHost();
-        final HttpHost proxy = route.getProxyHost();
-        final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy);
-        ClassicHttpResponse response = null;
-
-        final String authority = target.toHostString();
-        final ClassicHttpRequest connect = new BasicClassicHttpRequest("CONNECT", target, authority);
-        connect.setVersion(HttpVersion.HTTP_1_1);
-
-        this.proxyHttpProcessor.process(connect, null, context);
-
-        while (response == null) {
-            if (!execRuntime.isConnected()) {
-                execRuntime.connect(context);
-            }
-
-            connect.removeHeaders(HttpHeaders.PROXY_AUTHORIZATION);
-            this.authenticator.addAuthResponse(proxy, ChallengeType.PROXY, connect, proxyAuthExchange, context);
-
-            response = execRuntime.execute(connect, context);
-
-            final int status = response.getCode();
-            if (status < HttpStatus.SC_SUCCESS) {
-                throw new HttpException("Unexpected response to CONNECT request: " + new StatusLine(response));
-            }
-
-            if (config.isAuthenticationEnabled()) {
-                if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response,
-                        proxyAuthExchange, context)) {
-                    if (this.authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
-                            this.proxyAuthStrategy, proxyAuthExchange, context)) {
-                        // Retry request
-                        if (this.reuseStrategy.keepAlive(request, response, context)) {
-                            this.log.debug("Connection kept alive");
-                            // Consume response content
-                            final HttpEntity entity = response.getEntity();
-                            EntityUtils.consume(entity);
-                        } else {
-                            execRuntime.disconnect();
-                        }
-                        response = null;
-                    }
-                }
-            }
-        }
-
-        final int status = response.getCode();
-        if (status >= HttpStatus.SC_REDIRECTION) {
-
-            // Buffer response content
-            final HttpEntity entity = response.getEntity();
-            if (entity != null) {
-                response.setEntity(new BufferedHttpEntity(entity));
-            }
-
-            execRuntime.disconnect();
-            execRuntime.discardConnection();
-            throw new TunnelRefusedException("CONNECT refused by proxy: " +
-                    new StatusLine(response), response);
-        }
-
-        // How to decide on security of the tunnelled connection?
-        // The socket factory knows only about the segment to the proxy.
-        // Even if that is secure, the hop to the target may be insecure.
-        // Leave it to derived classes, consider insecure by default here.
-        return false;
-    }
-
-    /**
-     * Creates a tunnel to an intermediate proxy.
-     * This method is <i>not</i> implemented in this class.
-     * It just throws an exception here.
-     */
-    private boolean createTunnelToProxy(
-            final HttpRoute route,
-            final int hop,
-            final HttpClientContext context) throws HttpException {
-
-        // Have a look at createTunnelToTarget and replicate the parts
-        // you need in a custom derived class. If your proxies don't require
-        // authentication, it is not too hard. But for the stock version of
-        // HttpClient, we cannot make such simplifying assumptions and would
-        // have to include proxy authentication code. The HttpComponents team
-        // is currently not in a position to support rarely used code of this
-        // complexity. Feel free to submit patches that refactor the code in
-        // createTunnelToTarget to facilitate re-use for proxy tunnelling.
 
-        throw new HttpException("Proxy chains are not supported.");
-    }
-
-    private boolean needAuthentication(
-            final AuthExchange targetAuthExchange,
-            final AuthExchange proxyAuthExchange,
-            final HttpRoute route,
-            final ClassicHttpRequest request,
-            final HttpResponse response,
-            final HttpClientContext context) {
-        final RequestConfig config = context.getRequestConfig();
-        if (config.isAuthenticationEnabled()) {
-            final URIAuthority authority = request.getAuthority();
-            final String scheme = request.getScheme();
-            HttpHost target = authority != null ? new HttpHost(authority, scheme) : route.getTargetHost();;
-            if (target.getPort() < 0) {
-                target = new HttpHost(
-                        target.getHostName(),
-                        route.getTargetHost().getPort(),
-                        target.getSchemeName());
-            }
-            final boolean targetAuthRequested = this.authenticator.isChallenged(
-                    target, ChallengeType.TARGET, response, targetAuthExchange, context);
-
-            HttpHost proxy = route.getProxyHost();
-            // if proxy is not set use target host instead
-            if (proxy == null) {
-                proxy = route.getTargetHost();
-            }
-            final boolean proxyAuthRequested = this.authenticator.isChallenged(
-                    proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
-
-            if (targetAuthRequested) {
-                return this.authenticator.prepareAuthResponse(target, ChallengeType.TARGET, response,
-                        this.targetAuthStrategy, targetAuthExchange, context);
-            }
-            if (proxyAuthRequested) {
-                return this.authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
-                        this.proxyAuthStrategy, proxyAuthExchange, context);
-            }
-        }
-        return false;
     }
 
 }

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MinimalHttpClient.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MinimalHttpClient.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MinimalHttpClient.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/MinimalHttpClient.java Mon May  1 13:06:48 2017
@@ -28,30 +28,40 @@
 package org.apache.hc.client5.http.impl.sync;
 
 import java.io.IOException;
+import java.io.InterruptedIOException;
 
 import org.apache.hc.client5.http.CancellableAware;
 import org.apache.hc.client5.http.HttpRoute;
 import org.apache.hc.client5.http.config.Configurable;
 import org.apache.hc.client5.http.config.RequestConfig;
-import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy;
-import org.apache.hc.client5.http.impl.ExecSupport;
+import org.apache.hc.client5.http.impl.ConnectionShutdownException;
+import org.apache.hc.client5.http.impl.DefaultSchemePortResolver;
 import org.apache.hc.client5.http.io.HttpClientConnectionManager;
 import org.apache.hc.client5.http.protocol.ClientProtocolException;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
-import org.apache.hc.client5.http.sync.ExecChain;
+import org.apache.hc.client5.http.protocol.RequestClientConnControl;
 import org.apache.hc.client5.http.sync.ExecRuntime;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ConnectionReuseStrategy;
+import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.HttpHost;
 import org.apache.hc.core5.http.impl.DefaultConnectionReuseStrategy;
 import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
 import org.apache.hc.core5.http.protocol.BasicHttpContext;
+import org.apache.hc.core5.http.protocol.DefaultHttpProcessor;
 import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http.protocol.RequestContent;
+import org.apache.hc.core5.http.protocol.RequestTargetHost;
+import org.apache.hc.core5.http.protocol.RequestUserAgent;
 import org.apache.hc.core5.net.URIAuthority;
 import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.VersionInfo;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
@@ -66,16 +76,21 @@ public class MinimalHttpClient extends C
     private final Logger log = LogManager.getLogger(getClass());
 
     private final HttpClientConnectionManager connManager;
-    private final MinimalClientExec execChain;
+    private final ConnectionReuseStrategy reuseStrategy;
     private final HttpRequestExecutor requestExecutor;
+    private final HttpProcessor httpProcessor;
 
     MinimalHttpClient(final HttpClientConnectionManager connManager) {
         super();
         this.connManager = Args.notNull(connManager, "HTTP connection manager");
-        this.execChain = new MinimalClientExec(
-                DefaultConnectionReuseStrategy.INSTANCE,
-                DefaultConnectionKeepAliveStrategy.INSTANCE);
-        this.requestExecutor = new HttpRequestExecutor(DefaultConnectionReuseStrategy.INSTANCE);
+        this.reuseStrategy = DefaultConnectionReuseStrategy.INSTANCE;
+        this.requestExecutor = new HttpRequestExecutor(this.reuseStrategy);
+        this.httpProcessor = new DefaultHttpProcessor(
+                new RequestContent(),
+                new RequestTargetHost(),
+                new RequestClientConnControl(),
+                new RequestUserAgent(VersionInfo.getSoftwareInfo(
+                        "Apache-HttpClient", "org.apache.hc.client5", getClass())));
     }
 
     @Override
@@ -85,28 +100,70 @@ public class MinimalHttpClient extends C
             final HttpContext context) throws IOException {
         Args.notNull(target, "Target host");
         Args.notNull(request, "HTTP request");
+        if (request.getScheme() == null) {
+            request.setScheme(target.getSchemeName());
+        }
+        if (request.getAuthority() == null) {
+            request.setAuthority(new URIAuthority(target));
+        }
+        final HttpClientContext clientContext = HttpClientContext.adapt(
+                context != null ? context : new BasicHttpContext());
+        RequestConfig config = null;
+        if (request instanceof Configurable) {
+            config = ((Configurable) request).getConfig();
+        }
+        if (config != null) {
+            clientContext.setRequestConfig(config);
+        }
+
+        final HttpRoute route = new HttpRoute(target.getPort() > 0 ? target : new HttpHost(
+                target.getHostName(),
+                DefaultSchemePortResolver.INSTANCE.resolve(target),
+                target.getSchemeName()));
+
+        final ExecRuntime execRuntime = new ExecRuntimeImpl(log, connManager, requestExecutor,
+                request instanceof CancellableAware ? (CancellableAware) request : null);
         try {
-            if (request.getScheme() == null) {
-                request.setScheme(target.getSchemeName());
+            if (!execRuntime.isConnectionAcquired()) {
+                execRuntime.acquireConnection(route, null, clientContext);
             }
-            if (request.getAuthority() == null) {
-                request.setAuthority(new URIAuthority(target));
+            if (!execRuntime.isConnected()) {
+                execRuntime.connect(clientContext);
             }
-            final HttpClientContext localcontext = HttpClientContext.adapt(
-                    context != null ? context : new BasicHttpContext());
-            RequestConfig config = null;
-            if (request instanceof Configurable) {
-                config = ((Configurable) request).getConfig();
+
+            context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+            context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
+
+            httpProcessor.process(request, request.getEntity(), context);
+            final ClassicHttpResponse response = execRuntime.execute(request, clientContext);
+            httpProcessor.process(response, response.getEntity(), context);
+
+            if (reuseStrategy.keepAlive(request, response, context)) {
+                execRuntime.markConnectionReusable();
+            } else {
+                execRuntime.markConnectionNonReusable();
             }
-            if (config != null) {
-                localcontext.setRequestConfig(config);
+
+            // check for entity, release connection if possible
+            final HttpEntity entity = response.getEntity();
+            if (entity == null || !entity.isStreaming()) {
+                // connection not needed and (assumed to be) in re-usable state
+                execRuntime.releaseConnection();
+                return new CloseableHttpResponse(response, null);
+            } else {
+                ResponseEntityProxy.enchance(response, execRuntime);
+                return new CloseableHttpResponse(response, execRuntime);
             }
-            final ExecRuntime execRuntime = new ExecRuntimeImpl(log, connManager, requestExecutor,
-                    request instanceof CancellableAware ? (CancellableAware) request : null);
-            final ExecChain.Scope scope = new ExecChain.Scope(new HttpRoute(target), request, execRuntime, localcontext);
-            final ClassicHttpResponse response = this.execChain.execute(ExecSupport.copy(request), scope, null);
-            return CloseableHttpResponse.adapt(response);
+        } catch (final ConnectionShutdownException ex) {
+            final InterruptedIOException ioex = new InterruptedIOException("Connection has been shut down");
+            ioex.initCause(ex);
+            execRuntime.discardConnection();
+            throw ioex;
+        } catch (final RuntimeException | IOException ex) {
+            execRuntime.discardConnection();
+            throw ex;
         } catch (final HttpException httpException) {
+            execRuntime.discardConnection();
             throw new ClientProtocolException(httpException);
         }
     }

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ProtocolExec.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ProtocolExec.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ProtocolExec.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/ProtocolExec.java Mon May  1 13:06:48 2017
@@ -30,33 +30,46 @@ package org.apache.hc.client5.http.impl.
 import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Iterator;
 
 import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.StandardMethods;
+import org.apache.hc.client5.http.auth.AuthExchange;
+import org.apache.hc.client5.http.auth.ChallengeType;
 import org.apache.hc.client5.http.auth.CredentialsProvider;
 import org.apache.hc.client5.http.auth.CredentialsStore;
 import org.apache.hc.client5.http.auth.util.CredentialSupport;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.auth.HttpAuthenticator;
+import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
 import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.protocol.NonRepeatableRequestException;
 import org.apache.hc.client5.http.sync.ExecChain;
 import org.apache.hc.client5.http.sync.ExecChainHandler;
+import org.apache.hc.client5.http.sync.ExecRuntime;
 import org.apache.hc.client5.http.utils.URIUtils;
 import org.apache.hc.core5.annotation.Contract;
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
 import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpResponse;
 import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
 import org.apache.hc.core5.http.protocol.HttpCoreContext;
 import org.apache.hc.core5.http.protocol.HttpProcessor;
 import org.apache.hc.core5.net.URIAuthority;
 import org.apache.hc.core5.util.Args;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
 
 /**
  * Request executor in the request execution chain that is responsible
  * for implementation of HTTP specification requirements.
- * Internally this executor relies on a {@link HttpProcessor} to populate
- * requisite HTTP request headers, process HTTP response headers and processChallenge
- * session state in {@link HttpClientContext}.
  * <p>
  * Further responsibilities such as communication with the opposite
  * endpoint is delegated to the next executor in the request execution
@@ -68,11 +81,21 @@ import org.apache.hc.core5.util.Args;
 @Contract(threading = ThreadingBehavior.IMMUTABLE)
 final class ProtocolExec implements ExecChainHandler {
 
-    private final HttpProcessor httpProcessor;
+    private final Logger log = LogManager.getLogger(getClass());
 
-    public ProtocolExec(final HttpProcessor httpProcessor) {
-        Args.notNull(httpProcessor, "HTTP protocol processor");
-        this.httpProcessor = httpProcessor;
+    private final HttpProcessor httpProcessor;
+    private final AuthenticationStrategy targetAuthStrategy;
+    private final AuthenticationStrategy proxyAuthStrategy;
+    private final HttpAuthenticator authenticator;
+
+    public ProtocolExec(
+            final HttpProcessor httpProcessor,
+            final AuthenticationStrategy targetAuthStrategy,
+            final AuthenticationStrategy proxyAuthStrategy) {
+        this.httpProcessor = Args.notNull(httpProcessor, "HTTP protocol processor");
+        this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy");
+        this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy");
+        this.authenticator = new HttpAuthenticator();
     }
 
     @Override
@@ -85,46 +108,150 @@ final class ProtocolExec implements Exec
 
         final HttpRoute route = scope.route;
         final HttpClientContext context = scope.clientContext;
+        final ExecRuntime execRuntime = scope.execRuntime;
 
-        if (route.getProxyHost() != null && !route.isTunnelled()) {
-            try {
-                URI uri = request.getUri();
-                if (!uri.isAbsolute()) {
-                    final HttpHost target = route.getTargetHost();
-                    uri = URIUtils.rewriteURI(uri, target, true);
-                } else {
-                    uri = URIUtils.rewriteURI(uri);
+        try {
+            final HttpHost target = route.getTargetHost();
+            final HttpHost proxy = route.getProxyHost();
+            if (proxy != null && !route.isTunnelled()) {
+                try {
+                    URI uri = request.getUri();
+                    if (!uri.isAbsolute()) {
+                        uri = URIUtils.rewriteURI(uri, target, true);
+                    } else {
+                        uri = URIUtils.rewriteURI(uri);
+                    }
+                    request.setPath(uri.toASCIIString());
+                } catch (final URISyntaxException ex) {
+                    throw new ProtocolException("Invalid request URI: " + request.getRequestUri(), ex);
                 }
-                request.setPath(uri.toASCIIString());
-            } catch (final URISyntaxException ex) {
-                throw new ProtocolException("Invalid URI: " + request.getRequestUri(), ex);
             }
-        }
 
-        final URIAuthority authority = request.getAuthority();
-        if (authority != null) {
-            final CredentialsProvider credsProvider = context.getCredentialsProvider();
-            if (credsProvider instanceof CredentialsStore) {
-                CredentialSupport.extractFromAuthority(authority, (CredentialsStore) credsProvider);
+            final URIAuthority authority = request.getAuthority();
+            if (authority != null) {
+                final CredentialsProvider credsProvider = context.getCredentialsProvider();
+                if (credsProvider instanceof CredentialsStore) {
+                    CredentialSupport.extractFromAuthority(authority, (CredentialsStore) credsProvider);
+                }
             }
-        }
 
-        // Run request protocol interceptors
-        context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
-        context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+            final AuthExchange targetAuthExchange = context.getAuthExchange(target);
+            final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange();
 
-        this.httpProcessor.process(request, request.getEntity(), context);
+            for (int execCount = 1;; execCount++) {
 
-        final ClassicHttpResponse response = chain.proceed(request, scope);
-        try {
-            // Run response protocol interceptors
-            context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
-            this.httpProcessor.process(response, response.getEntity(), context);
-            return response;
+                if (execCount > 1) {
+                    final HttpEntity entity = request.getEntity();
+                    if (entity != null && !entity.isRepeatable()) {
+                        throw new NonRepeatableRequestException("Cannot retry request " +
+                                "with a non-repeatable request entity.");
+                    }
+                }
+
+                // Run request protocol interceptors
+                context.setAttribute(HttpClientContext.HTTP_ROUTE, route);
+                context.setAttribute(HttpCoreContext.HTTP_REQUEST, request);
+
+                httpProcessor.process(request, request.getEntity(), context);
+
+                if (!request.containsHeader(HttpHeaders.AUTHORIZATION)) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("Target auth state: " + targetAuthExchange.getState());
+                    }
+                    authenticator.addAuthResponse(target, ChallengeType.TARGET, request, targetAuthExchange, context);
+                }
+                if (!request.containsHeader(HttpHeaders.PROXY_AUTHORIZATION) && !route.isTunnelled()) {
+                    if (log.isDebugEnabled()) {
+                        log.debug("Proxy auth state: " + proxyAuthExchange.getState());
+                    }
+                    authenticator.addAuthResponse(proxy, ChallengeType.PROXY, request, proxyAuthExchange, context);
+                }
+
+                final ClassicHttpResponse response = chain.proceed(request, scope);
+
+                context.setAttribute(HttpCoreContext.HTTP_RESPONSE, response);
+                httpProcessor.process(response, response.getEntity(), context);
+
+                if (request.getMethod().equalsIgnoreCase(StandardMethods.TRACE.name())) {
+                    // Do not perform authentication for TRACE request
+                    return response;
+                }
+
+                if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, context)) {
+                    // Make sure the response body is fully consumed, if present
+                    final HttpEntity entity = response.getEntity();
+                    if (execRuntime.isConnectionReusable()) {
+                        EntityUtils.consume(entity);
+                    } else {
+                        execRuntime.disconnect();
+                        if (proxyAuthExchange.getState() == AuthExchange.State.SUCCESS
+                                && proxyAuthExchange.getAuthScheme() != null
+                                && proxyAuthExchange.getAuthScheme().isConnectionBased()) {
+                            log.debug("Resetting proxy auth state");
+                            proxyAuthExchange.reset();
+                        }
+                        if (targetAuthExchange.getState() == AuthExchange.State.SUCCESS
+                                && targetAuthExchange.getAuthScheme() != null
+                                && targetAuthExchange.getAuthScheme().isConnectionBased()) {
+                            log.debug("Resetting target auth state");
+                            targetAuthExchange.reset();
+                        }
+                    }
+                    // Reset request headers
+                    final ClassicHttpRequest original = scope.originalRequest;
+                    request.setHeaders();
+                    for (final Iterator<Header> it = original.headerIterator(); it.hasNext(); ) {
+                        request.addHeader(it.next());
+                    }
+                } else {
+                    return response;
+                }
+            }
         } catch (final RuntimeException | HttpException | IOException ex) {
-            response.close();
+            execRuntime.discardConnection();
             throw ex;
         }
     }
 
+    private boolean needAuthentication(
+            final AuthExchange targetAuthExchange,
+            final AuthExchange proxyAuthExchange,
+            final HttpRoute route,
+            final ClassicHttpRequest request,
+            final HttpResponse response,
+            final HttpClientContext context) {
+        final RequestConfig config = context.getRequestConfig();
+        if (config.isAuthenticationEnabled()) {
+            final URIAuthority authority = request.getAuthority();
+            final String scheme = request.getScheme();
+            HttpHost target = authority != null ? new HttpHost(authority, scheme) : route.getTargetHost();;
+            if (target.getPort() < 0) {
+                target = new HttpHost(
+                        target.getHostName(),
+                        route.getTargetHost().getPort(),
+                        target.getSchemeName());
+            }
+            final boolean targetAuthRequested = authenticator.isChallenged(
+                    target, ChallengeType.TARGET, response, targetAuthExchange, context);
+
+            HttpHost proxy = route.getProxyHost();
+            // if proxy is not set use target host instead
+            if (proxy == null) {
+                proxy = route.getTargetHost();
+            }
+            final boolean proxyAuthRequested = authenticator.isChallenged(
+                    proxy, ChallengeType.PROXY, response, proxyAuthExchange, context);
+
+            if (targetAuthRequested) {
+                return authenticator.prepareAuthResponse(target, ChallengeType.TARGET, response,
+                        targetAuthStrategy, targetAuthExchange, context);
+            }
+            if (proxyAuthRequested) {
+                return authenticator.prepareAuthResponse(proxy, ChallengeType.PROXY, response,
+                        proxyAuthStrategy, proxyAuthExchange, context);
+            }
+        }
+        return false;
+    }
+
 }

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RequestEntityProxy.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RequestEntityProxy.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RequestEntityProxy.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RequestEntityProxy.java Mon May  1 13:06:48 2017
@@ -55,20 +55,6 @@ class RequestEntityProxy implements Http
         return entity instanceof RequestEntityProxy;
     }
 
-    static boolean isRepeatable(final ClassicHttpRequest request) {
-        final HttpEntity entity = request.getEntity();
-        if (entity != null) {
-            if (isEnhanced(entity)) {
-                final RequestEntityProxy proxy = (RequestEntityProxy) entity;
-                if (!proxy.isConsumed()) {
-                    return true;
-                }
-            }
-            return entity.isRepeatable();
-        }
-        return true;
-    }
-
     private final HttpEntity original;
     private boolean consumed = false;
 
@@ -87,7 +73,11 @@ class RequestEntityProxy implements Http
 
     @Override
     public boolean isRepeatable() {
-        return original.isRepeatable();
+        if (!consumed) {
+            return true;
+        } else {
+            return original.isRepeatable();
+        }
     }
 
     @Override

Modified: httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RetryExec.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RetryExec.java?rev=1793325&r1=1793324&r2=1793325&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RetryExec.java (original)
+++ httpcomponents/httpclient/trunk/httpclient5/src/main/java/org/apache/hc/client5/http/impl/sync/RetryExec.java Mon May  1 13:06:48 2017
@@ -40,6 +40,7 @@ import org.apache.hc.core5.annotation.Co
 import org.apache.hc.core5.annotation.ThreadingBehavior;
 import org.apache.hc.core5.http.ClassicHttpRequest;
 import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.HttpEntity;
 import org.apache.hc.core5.http.HttpException;
 import org.apache.hc.core5.http.NoHttpResponseException;
 import org.apache.hc.core5.util.Args;
@@ -99,7 +100,8 @@ final class RetryExec implements ExecCha
                     if (this.log.isDebugEnabled()) {
                         this.log.debug(ex.getMessage(), ex);
                     }
-                    if (!RequestEntityProxy.isRepeatable(request)) {
+                    final HttpEntity entity = request.getEntity();
+                    if (entity != null && !entity.isRepeatable()) {
                         this.log.debug("Cannot retry non-repeatable request");
                         throw new NonRepeatableRequestException("Cannot retry request " +
                                 "with a non-repeatable request entity", ex);

Added: httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java?rev=1793325&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java (added)
+++ httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java Mon May  1 13:06:48 2017
@@ -0,0 +1,359 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.sync;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.Collections;
+import java.util.Map;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.RouteInfo;
+import org.apache.hc.client5.http.auth.AuthChallenge;
+import org.apache.hc.client5.http.auth.AuthScheme;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.ChallengeType;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.entity.EntityBuilder;
+import org.apache.hc.client5.http.impl.auth.BasicScheme;
+import org.apache.hc.client5.http.protocol.AuthenticationStrategy;
+import org.apache.hc.client5.http.protocol.HttpClientContext;
+import org.apache.hc.client5.http.sync.ExecChain;
+import org.apache.hc.client5.http.sync.ExecRuntime;
+import org.apache.hc.client5.http.sync.methods.HttpGet;
+import org.apache.hc.core5.http.ClassicHttpRequest;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ConnectionReuseStrategy;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+@SuppressWarnings({"boxing","static-access"}) // test code
+public class TestConnectExec {
+
+    @Mock
+    private ConnectionReuseStrategy reuseStrategy;
+    @Mock
+    private HttpProcessor proxyHttpProcessor;
+    @Mock
+    private AuthenticationStrategy proxyAuthStrategy;
+    @Mock
+    private ExecRuntime execRuntime;
+    @Mock
+    private ExecChain execChain;
+
+    private ConnectExec exec;
+    private HttpHost target;
+    private HttpHost proxy;
+
+    @Before
+    public void setup() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        exec = new ConnectExec(reuseStrategy, proxyHttpProcessor, proxyAuthStrategy);
+        target = new HttpHost("foo", 80);
+        proxy = new HttpHost("bar", 8888);
+    }
+
+    @Test
+    public void testExecAcquireConnection() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+        final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
+        response.setEntity(EntityBuilder.create()
+                .setStream(new ByteArrayInputStream(new byte[]{}))
+                .build());
+        context.setUserToken("Blah");
+
+        Mockito.when(execRuntime.isConnectionAcquired()).thenReturn(false);
+        Mockito.when(execRuntime.execute(
+                Mockito.same(request),
+                Mockito.<HttpClientContext>any())).thenReturn(response);
+        Mockito.when(reuseStrategy.keepAlive(
+                Mockito.same(request),
+                Mockito.same(response),
+                Mockito.<HttpClientContext>any())).thenReturn(false);
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+        Mockito.verify(execRuntime).acquireConnection(route, "Blah", context);
+        Mockito.verify(execRuntime).connect(context);
+    }
+
+    @Test
+    public void testEstablishDirectRoute() throws Exception {
+        final HttpRoute route = new HttpRoute(target);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+
+        Mockito.verify(execRuntime).connect(context);
+        Mockito.verify(execRuntime, Mockito.never()).execute(Mockito.<ClassicHttpRequest>any(), Mockito.<HttpClientContext>any());
+    }
+
+    @Test
+    public void testEstablishRouteDirectProxy() throws Exception {
+        final HttpRoute route = new HttpRoute(target, null, proxy, false);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+
+        Mockito.verify(execRuntime).connect(context);
+        Mockito.verify(execRuntime, Mockito.never()).execute(Mockito.<ClassicHttpRequest>any(), Mockito.<HttpClientContext>any());
+    }
+
+    @Test
+    public void testEstablishRouteViaProxyTunnel() throws Exception {
+        final HttpRoute route = new HttpRoute(target, null, proxy, true);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+        final ClassicHttpResponse response = new BasicClassicHttpResponse(200, "OK");
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+        Mockito.when(execRuntime.execute(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(response);
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+
+        Mockito.verify(execRuntime).connect(context);
+        final ArgumentCaptor<ClassicHttpRequest> reqCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class);
+        Mockito.verify(execRuntime).execute(
+                reqCaptor.capture(),
+                Mockito.same(context));
+        final HttpRequest connect = reqCaptor.getValue();
+        Assert.assertNotNull(connect);
+        Assert.assertEquals("CONNECT", connect.getMethod());
+        Assert.assertEquals(HttpVersion.HTTP_1_1, connect.getVersion());
+        Assert.assertEquals("foo:80", connect.getRequestUri());
+    }
+
+    @Test(expected = HttpException.class)
+    public void testEstablishRouteViaProxyTunnelUnexpectedResponse() throws Exception {
+        final HttpRoute route = new HttpRoute(target, null, proxy, true);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+        final ClassicHttpResponse response = new BasicClassicHttpResponse(101, "Lost");
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+        Mockito.when(execRuntime.execute(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(response);
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+    }
+
+    @Test(expected = HttpException.class)
+    public void testEstablishRouteViaProxyTunnelFailure() throws Exception {
+        final HttpRoute route = new HttpRoute(target, null, proxy, true);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+        final ClassicHttpResponse response = new BasicClassicHttpResponse(500, "Boom");
+        response.setEntity(new StringEntity("Ka-boom"));
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+        Mockito.when(execRuntime.execute(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(response);
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        try {
+            exec.execute(request, scope, execChain);
+        } catch (final TunnelRefusedException ex) {
+            final ClassicHttpResponse r = ex.getResponse();
+            Assert.assertEquals("Ka-boom", EntityUtils.toString(r.getEntity()));
+            Mockito.verify(execRuntime).disconnect();
+            Mockito.verify(execRuntime).discardConnection();
+            throw ex;
+        }
+    }
+
+    @Test
+    public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengePersistentConnection() throws Exception {
+        final HttpRoute route = new HttpRoute(target, null, proxy, true);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+        final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
+        response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, "Basic realm=test");
+        final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
+        response1.setEntity(EntityBuilder.create()
+                .setStream(instream1)
+                .build());
+        final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
+
+        final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("user", "pass".toCharArray()));
+        context.setCredentialsProvider(credentialsProvider);
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+        Mockito.when(reuseStrategy.keepAlive(
+                Mockito.same(request),
+                Mockito.<HttpResponse>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(Boolean.TRUE);
+        Mockito.when(execRuntime.execute(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
+
+        Mockito.when(proxyAuthStrategy.select(
+                Mockito.eq(ChallengeType.PROXY),
+                Mockito.<Map<String, AuthChallenge>>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+
+        Mockito.verify(execRuntime).connect(context);
+        Mockito.verify(instream1).close();
+    }
+
+    @Test
+    public void testEstablishRouteViaProxyTunnelRetryOnAuthChallengeNonPersistentConnection() throws Exception {
+        final HttpRoute route = new HttpRoute(target, null, proxy, true);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+        final ClassicHttpResponse response1 = new BasicClassicHttpResponse(407, "Huh?");
+        response1.setHeader(HttpHeaders.PROXY_AUTHENTICATE, "Basic realm=test");
+        final InputStream instream1 = Mockito.spy(new ByteArrayInputStream(new byte[] {1, 2, 3}));
+        response1.setEntity(EntityBuilder.create()
+                .setStream(instream1)
+                .build());
+        final ClassicHttpResponse response2 = new BasicClassicHttpResponse(200, "OK");
+
+        final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+        credentialsProvider.setCredentials(new AuthScope(proxy), new UsernamePasswordCredentials("user", "pass".toCharArray()));
+        context.setCredentialsProvider(credentialsProvider);
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+        Mockito.when(reuseStrategy.keepAlive(
+                Mockito.same(request),
+                Mockito.<HttpResponse>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(Boolean.FALSE);
+        Mockito.when(execRuntime.execute(
+                Mockito.<ClassicHttpRequest>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(response1, response2);
+
+        Mockito.when(proxyAuthStrategy.select(
+                Mockito.eq(ChallengeType.PROXY),
+                Mockito.<Map<String, AuthChallenge>>any(),
+                Mockito.<HttpClientContext>any())).thenReturn(Collections.<AuthScheme>singletonList(new BasicScheme()));
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+
+        Mockito.verify(execRuntime).connect(context);
+        Mockito.verify(instream1, Mockito.never()).close();
+        Mockito.verify(execRuntime).disconnect();
+    }
+
+    @Test(expected = HttpException.class)
+    public void testEstablishRouteViaProxyTunnelMultipleHops() throws Exception {
+        final HttpHost proxy1 = new HttpHost("this", 8888);
+        final HttpHost proxy2 = new HttpHost("that", 8888);
+        final HttpRoute route = new HttpRoute(target, null, new HttpHost[] {proxy1, proxy2},
+                true, RouteInfo.TunnelType.TUNNELLED, RouteInfo.LayerType.LAYERED);
+        final HttpClientContext context = new HttpClientContext();
+        final ClassicHttpRequest request = new HttpGet("http://bar/test");
+
+        final ConnectionState connectionState = new ConnectionState();
+        Mockito.doAnswer(connectionState.connectAnswer()).when(execRuntime).connect(Mockito.<HttpClientContext>any());
+        Mockito.when(execRuntime.isConnected()).thenAnswer(connectionState.isConnectedAnswer());
+
+        final ExecChain.Scope scope = new ExecChain.Scope(route, request, execRuntime, context);
+        exec.execute(request, scope, execChain);
+    }
+
+    static class ConnectionState {
+
+        private boolean connected;
+
+        public Answer connectAnswer() {
+
+            return new Answer() {
+
+                @Override
+                public Object answer(final InvocationOnMock invocationOnMock) throws Throwable {
+                    connected = true;
+                    return null;
+                }
+
+            };
+        }
+
+        public Answer<Boolean> isConnectedAnswer() {
+
+            return new Answer<Boolean>() {
+
+                @Override
+                public Boolean answer(final InvocationOnMock invocationOnMock) throws Throwable {
+                    return connected;
+                }
+
+            };
+
+        };
+    }
+
+}

Propchange: httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java
------------------------------------------------------------------------------
    svn:keywords = Date Revision

Propchange: httpcomponents/httpclient/trunk/httpclient5/src/test/java/org/apache/hc/client5/http/impl/sync/TestConnectExec.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain