diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java index 9beb5f0054..2297472e7d 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/RequestSupport.java @@ -33,6 +33,7 @@ import java.util.List; import org.apache.hc.core5.annotation.Internal; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.net.PathSegmentCodec; import org.apache.hc.core5.net.PercentCodec; @@ -93,8 +94,45 @@ static List optimizePathSegments(final List inputSegments) { } } + /** + * @since 5.7 + */ + public static Supplier pathPrefixSupplier(final String path) { + return new Supplier() { + + private volatile String cached; + + @Override + public String get() { + // This is racy but the class is not meant to be used + // concurrently by multiple threads + String value = cached; + if (value == null) { + value = extractPathPrefix(path); + cached = value; + } + return value; + } + + }; + } + + /** + * @deprecated Do not use. + */ + @Deprecated public static String extractPathPrefix(final HttpRequest request) { - String path = request.getPath(); + return extractPathPrefix(request.getPath()); + } + + /** + * @since 5.7 + */ + public static String extractPathPrefix(final String requestPath) { + if (requestPath.isEmpty()) { + return "/"; + } + String path = requestPath; boolean hasSegments = false; boolean requiresOptimization = false; if (!path.startsWith("/")) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java index 97c600a981..b3f6442677 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncConnectExec.java @@ -395,7 +395,7 @@ private void createTunnel( final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext); + authCacheKeeper.loadPreemptivelyFromCache(proxy, null, proxyAuthExchange, clientContext); } final AsyncClientExchangeHandler internalExchangeHandler = new AsyncClientExchangeHandler() { @@ -539,9 +539,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -550,7 +550,7 @@ private boolean needAuthentication( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnResponse(proxy, null, proxyAuthExchange, context); } return updated; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java index 3a7de2326a..1a33328de2 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/AsyncProtocolExec.java @@ -50,6 +50,8 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpException; @@ -84,24 +86,38 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { private static final Logger LOG = LoggerFactory.getLogger(AsyncProtocolExec.class); + private static final Factory> DEFAULT_PREFIX_EXTRACTOR_FACTORY = r -> + RequestSupport.pathPrefixSupplier(r.getPath()); + private final AuthenticationStrategy targetAuthStrategy; private final AuthenticationStrategy proxyAuthStrategy; private final AuthenticationHandler authenticator; private final SchemePortResolver schemePortResolver; + private final Factory> prefixExtractorFactory; private final AuthCacheKeeper authCacheKeeper; AsyncProtocolExec( final AuthenticationStrategy targetAuthStrategy, final AuthenticationStrategy proxyAuthStrategy, final SchemePortResolver schemePortResolver, + final Factory> prefixExtractorFactory, final boolean authCachingDisabled) { this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy"); this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); this.authenticator = new AuthenticationHandler(); this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; + this.prefixExtractorFactory = prefixExtractorFactory != null ? prefixExtractorFactory : DEFAULT_PREFIX_EXTRACTOR_FACTORY; this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver); } + public AsyncProtocolExec( + final AuthenticationStrategy targetAuthStrategy, + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled) { + this(targetAuthStrategy, proxyAuthStrategy, schemePortResolver, DEFAULT_PREFIX_EXTRACTOR_FACTORY, authCachingDisabled); + } + @Override public void execute( final HttpRequest userRequest, @@ -148,33 +164,33 @@ public void execute( request.getScheme(), authority.getHostName(), schemePortResolver.resolve(request.getScheme(), authority)); - final String pathPrefix = RequestSupport.extractPathPrefix(request); + final Supplier pathPrefixSupplier = prefixExtractorFactory.create(request); final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target); final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); if (!targetAuthExchange.isConnectionBased() && targetAuthExchange.getPathPrefix() != null && - !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) { + !pathPrefixSupplier.get().startsWith(targetAuthExchange.getPathPrefix())) { // force re-authentication if the current path prefix does not match // that of the previous authentication exchange. targetAuthExchange.reset(); } if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, clientContext); + authCacheKeeper.loadPreemptivelyFromCache(target, pathPrefixSupplier, targetAuthExchange, clientContext); if (proxy != null) { - authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, clientContext); + authCacheKeeper.loadPreemptivelyFromCache(proxy, null, proxyAuthExchange, clientContext); } } final AtomicBoolean challenged = new AtomicBoolean(false); - internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange, + internalExecute(target, pathPrefixSupplier, targetAuthExchange, proxyAuthExchange, challenged, request, entityProducer, scope, chain, asyncExecCallback); } private void internalExecute( final HttpHost target, - final String pathPrefix, + final Supplier pathPrefixSupplier, final AuthExchange targetAuthExchange, final AuthExchange proxyAuthExchange, final AtomicBoolean challenged, @@ -220,7 +236,7 @@ public AsyncDataConsumer handleResponse( // A 407 received through a tunnel comes from the origin, not from the proxy route.isTunnelled() ? null : proxy, target, - pathPrefix, + pathPrefixSupplier, response, clientContext)) { challenged.set(true); @@ -272,7 +288,7 @@ public void completed() { if (entityProducer != null) { entityProducer.releaseResources(); } - internalExecute(target, pathPrefix, targetAuthExchange, proxyAuthExchange, + internalExecute(target, pathPrefixSupplier, targetAuthExchange, proxyAuthExchange, challenged, request, entityProducer, scope, chain, asyncExecCallback); } catch (final HttpException | IOException ex) { asyncExecCallback.failed(ex); @@ -303,7 +319,7 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpHost target, - final String pathPrefix, + final Supplier pathPrefixSupplier, final HttpResponse response, final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); @@ -314,9 +330,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (targetAuthRequested) { - authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context); + authCacheKeeper.updateCacheOnChallenge(target, pathPrefixSupplier, targetAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context); + authCacheKeeper.updateCacheOnNoChallenge(target, pathPrefixSupplier, targetAuthExchange, context); } } @@ -326,9 +342,9 @@ private boolean needAuthentication( if (proxy != null && authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -339,10 +355,10 @@ private boolean needAuthentication( targetAuthStrategy, targetAuthExchange, context); if (!targetAuthExchange.isConnectionBased() && targetAuthExchange.getPathPrefix() == null) { - targetAuthExchange.setPathPrefix(pathPrefix); + targetAuthExchange.setPathPrefix(pathPrefixSupplier.get()); } if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context); + authCacheKeeper.updateCacheOnResponse(target, pathPrefixSupplier, targetAuthExchange, context); } } if (proxyAuthRequested || proxyMutualAuthRequired) { @@ -350,7 +366,7 @@ private boolean needAuthentication( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnResponse(proxy, null, proxyAuthExchange, context); } } return targetNeedsAuth || proxyNeedsAuth; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java index 4222c96648..07b84b9304 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java @@ -36,6 +36,7 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.protocol.HttpContext; import org.slf4j.Logger; @@ -58,39 +59,88 @@ public AuthCacheKeeper(final SchemePortResolver schemePortResolver) { this.schemePortResolver = schemePortResolver; } + /** + * @since 5.7 + */ + public void updateCacheOnChallenge(final HttpHost host, + final Supplier pathPrefixSupplier, + final AuthExchange authExchange, + final HttpContext context) { + clearCache(host, pathPrefixSupplier, HttpClientContext.cast(context)); + } + + /** + * @deprecated Do not use. + */ + @Deprecated public void updateOnChallenge(final HttpHost host, final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { - clearCache(host, pathPrefix, HttpClientContext.cast(context)); + updateCacheOnChallenge(host, pathPrefix != null ? () -> pathPrefix : null, authExchange, context); + } + + /** + * @since 5.7 + */ + public void updateCacheOnNoChallenge(final HttpHost host, + final Supplier pathPrefixSupplier, + final AuthExchange authExchange, + final HttpContext context) { + if (authExchange.getState() == AuthExchange.State.SUCCESS) { + updateCache(host, pathPrefixSupplier, authExchange.getAuthScheme(), HttpClientContext.cast(context)); + } } + /** + * @deprecated Do not use. + */ + @Deprecated public void updateOnNoChallenge(final HttpHost host, final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { - if (authExchange.getState() == AuthExchange.State.SUCCESS) { - updateCache(host, pathPrefix, authExchange.getAuthScheme(), HttpClientContext.cast(context)); - } + updateCacheOnNoChallenge(host, pathPrefix != null ? () -> pathPrefix : null, authExchange, context); } - public void updateOnResponse(final HttpHost host, - final String pathPrefix, - final AuthExchange authExchange, - final HttpContext context) { + /** + * @since 5.7 + */ + public void updateCacheOnResponse(final HttpHost host, + final Supplier pathPrefixSupplier, + final AuthExchange authExchange, + final HttpContext context) { if (authExchange.getState() == AuthExchange.State.FAILURE) { - clearCache(host, pathPrefix, HttpClientContext.cast(context)); + clearCache(host, pathPrefixSupplier, HttpClientContext.cast(context)); } } - public void loadPreemptively(final HttpHost host, + /** + * @deprecated Do not use. + */ + @Deprecated + public void updateOnResponse(final HttpHost host, final String pathPrefix, final AuthExchange authExchange, final HttpContext context) { + updateCacheOnResponse(host, pathPrefix != null ? () -> pathPrefix : null, authExchange, context); + } + + /** + * @since 5.7 + */ + public void loadPreemptivelyFromCache(final HttpHost host, + final Supplier pathPrefixSupplier, + final AuthExchange authExchange, + final HttpContext context) { + final HttpClientContext clientContext = HttpClientContext.cast(context); + if (clientContext.getAuthCache() == null) { + return; + } if (authExchange.getState() == AuthExchange.State.UNCHALLENGED) { - AuthScheme authScheme = loadFromCache(host, pathPrefix, HttpClientContext.cast(context)); - if (authScheme == null && pathPrefix != null) { - authScheme = loadFromCache(host, null, HttpClientContext.cast(context)); + AuthScheme authScheme = loadFromCache(host, pathPrefixSupplier, clientContext); + if (authScheme == null && pathPrefixSupplier != null) { + authScheme = loadFromCache(host, null, clientContext); } if (authScheme != null) { authExchange.select(authScheme); @@ -98,11 +148,23 @@ public void loadPreemptively(final HttpHost host, } } + /** + * @deprecated Do not use. + */ + @Deprecated + public void loadPreemptively(final HttpHost host, + final String pathPrefix, + final AuthExchange authExchange, + final HttpContext context) { + loadPreemptivelyFromCache(host, pathPrefix != null ? () -> pathPrefix : null, authExchange, context); + } + private AuthScheme loadFromCache(final HttpHost host, - final String pathPrefix, + final Supplier pathPrefixSupplier, final HttpClientContext clientContext) { final AuthCache authCache = clientContext.getAuthCache(); if (authCache != null) { + final String pathPrefix = pathPrefixSupplier != null ? pathPrefixSupplier.get() : null; final AuthScheme authScheme = authCache.get(host, pathPrefix); if (authScheme != null) { if (LOG.isDebugEnabled()) { @@ -117,7 +179,7 @@ private AuthScheme loadFromCache(final HttpHost host, } private void updateCache(final HttpHost host, - final String pathPrefix, + final Supplier pathPrefixSupplier, final AuthScheme authScheme, final HttpClientContext clientContext) { final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null; @@ -127,6 +189,7 @@ private void updateCache(final HttpHost host, authCache = new BasicAuthCache(schemePortResolver); clientContext.setAuthCache(authCache); } + final String pathPrefix = pathPrefixSupplier != null ? pathPrefixSupplier.get() : null; if (LOG.isDebugEnabled()) { final String exchangeId = clientContext.getExchangeId(); LOG.debug("{} Caching '{}' auth scheme for {}{}", exchangeId, authScheme.getName(), host, @@ -137,10 +200,11 @@ private void updateCache(final HttpHost host, } private void clearCache(final HttpHost host, - final String pathPrefix, + final Supplier pathPrefixSupplier, final HttpClientContext clientContext) { final AuthCache authCache = clientContext.getAuthCache(); if (authCache != null) { + final String pathPrefix = pathPrefixSupplier != null ? pathPrefixSupplier.get() : null; if (LOG.isDebugEnabled()) { final String exchangeId = clientContext.getExchangeId(); LOG.debug("{} Clearing cached auth scheme for {}{}", exchangeId, host, diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java index 54e37b32a3..532fbd7d78 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ConnectExec.java @@ -211,7 +211,7 @@ private ClassicHttpResponse createTunnelToTarget( final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy); if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context); + authCacheKeeper.loadPreemptivelyFromCache(proxy, null, proxyAuthExchange, context); } ClassicHttpResponse response = null; @@ -250,9 +250,9 @@ private ClassicHttpResponse createTunnelToTarget( if (authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -261,7 +261,7 @@ private ClassicHttpResponse createTunnelToTarget( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnResponse(proxy, null, proxyAuthExchange, context); } if (updated) { if (this.reuseStrategy.keepAlive(connect, response, context)) { diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java index cc73b224e5..8edd864505 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ProtocolExec.java @@ -49,6 +49,8 @@ import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.Internal; import org.apache.hc.core5.annotation.ThreadingBehavior; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; import org.apache.hc.core5.http.Header; @@ -56,6 +58,7 @@ 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.Method; import org.apache.hc.core5.http.ProtocolException; @@ -83,22 +86,36 @@ public final class ProtocolExec implements ExecChainHandler { private static final Logger LOG = LoggerFactory.getLogger(ProtocolExec.class); + private static final Factory> DEFAULT_PREFIX_EXTRACTOR_FACTORY = r -> + RequestSupport.pathPrefixSupplier(r.getPath()); + private final AuthenticationStrategy targetAuthStrategy; private final AuthenticationStrategy proxyAuthStrategy; private final AuthenticationHandler authenticator; private final SchemePortResolver schemePortResolver; + private final Factory> prefixExtractorFactory; private final AuthCacheKeeper authCacheKeeper; - public ProtocolExec( + ProtocolExec( final AuthenticationStrategy targetAuthStrategy, final AuthenticationStrategy proxyAuthStrategy, final SchemePortResolver schemePortResolver, + final Factory> prefixExtractorFactory, final boolean authCachingDisabled) { this.targetAuthStrategy = Args.notNull(targetAuthStrategy, "Target authentication strategy"); this.proxyAuthStrategy = Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); this.authenticator = new AuthenticationHandler(); this.schemePortResolver = schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE; this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(this.schemePortResolver); + this.prefixExtractorFactory = prefixExtractorFactory != null ? prefixExtractorFactory : DEFAULT_PREFIX_EXTRACTOR_FACTORY; + } + + public ProtocolExec( + final AuthenticationStrategy targetAuthStrategy, + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled) { + this(targetAuthStrategy, proxyAuthStrategy, schemePortResolver, DEFAULT_PREFIX_EXTRACTOR_FACTORY, authCachingDisabled); } @Override @@ -151,23 +168,23 @@ public ClassicHttpResponse execute( request.getScheme(), authority.getHostName(), schemePortResolver.resolve(request.getScheme(), authority)); - final String pathPrefix = RequestSupport.extractPathPrefix(request); + final Supplier pathPrefixSupplier = prefixExtractorFactory.create(request); final AuthExchange targetAuthExchange = context.getAuthExchange(target); final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange(); if (!targetAuthExchange.isConnectionBased() && targetAuthExchange.getPathPrefix() != null && - !pathPrefix.startsWith(targetAuthExchange.getPathPrefix())) { + !pathPrefixSupplier.get().startsWith(targetAuthExchange.getPathPrefix())) { // force re-authentication if the current path prefix does not match // that of the previous authentication exchange. targetAuthExchange.reset(); } - if (authCacheKeeper != null) { - authCacheKeeper.loadPreemptively(target, pathPrefix, targetAuthExchange, context); + if (authCacheKeeper != null && context.getAuthCache() != null) { + authCacheKeeper.loadPreemptivelyFromCache(target, pathPrefixSupplier, targetAuthExchange, context); if (proxy != null) { - authCacheKeeper.loadPreemptively(proxy, null, proxyAuthExchange, context); + authCacheKeeper.loadPreemptivelyFromCache(proxy, null, proxyAuthExchange, context); } } @@ -210,7 +227,7 @@ public ClassicHttpResponse execute( // A 407 received through a tunnel comes from the origin, not from the proxy route.isTunnelled() ? null : proxy, target, - pathPrefix, + pathPrefixSupplier, response, context)) { // Make sure the response body is fully consumed, if present @@ -264,7 +281,7 @@ private boolean needAuthentication( final AuthExchange proxyAuthExchange, final HttpHost proxy, final HttpHost target, - final String pathPrefix, + final Supplier pathPrefixSupplier, final HttpResponse response, final HttpClientContext context) throws AuthenticationException, MalformedChallengeException { final RequestConfig config = context.getRequestConfigOrDefault(); @@ -275,9 +292,9 @@ private boolean needAuthentication( if (authCacheKeeper != null) { if (targetAuthRequested) { - authCacheKeeper.updateOnChallenge(target, pathPrefix, targetAuthExchange, context); + authCacheKeeper.updateCacheOnChallenge(target, pathPrefixSupplier, targetAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(target, pathPrefix, targetAuthExchange, context); + authCacheKeeper.updateCacheOnNoChallenge(target, pathPrefixSupplier, targetAuthExchange, context); } } @@ -287,9 +304,9 @@ private boolean needAuthentication( if (proxy != null && authCacheKeeper != null) { if (proxyAuthRequested) { - authCacheKeeper.updateOnChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnChallenge(proxy, null, proxyAuthExchange, context); } else { - authCacheKeeper.updateOnNoChallenge(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnNoChallenge(proxy, null, proxyAuthExchange, context); } } @@ -300,10 +317,10 @@ private boolean needAuthentication( targetAuthStrategy, targetAuthExchange, context); if (!targetAuthExchange.isConnectionBased() && targetAuthExchange.getPathPrefix() == null) { - targetAuthExchange.setPathPrefix(pathPrefix); + targetAuthExchange.setPathPrefix(pathPrefixSupplier.get()); } if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(target, pathPrefix, targetAuthExchange, context); + authCacheKeeper.updateCacheOnResponse(target, pathPrefixSupplier, targetAuthExchange, context); } } if (proxyAuthRequested || proxyMutualAuthRequired) { @@ -311,7 +328,7 @@ private boolean needAuthentication( proxyAuthStrategy, proxyAuthExchange, context); if (authCacheKeeper != null) { - authCacheKeeper.updateOnResponse(proxy, null, proxyAuthExchange, context); + authCacheKeeper.updateCacheOnResponse(proxy, null, proxyAuthExchange, context); } } return targetNeedsAuth || proxyNeedsAuth; diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java index e3912f4d87..227d0c7cff 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/TestRequestSupport.java @@ -26,7 +26,7 @@ */ package org.apache.hc.client5.http.impl; -import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.function.Supplier; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -37,36 +37,45 @@ class TestRequestSupport { @Test void testPathPrefixExtraction() { - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/../aaaa/"))); - Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/cccc"))); - Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/.."))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb?////"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa//bbbb"))); - Assertions.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aa%2faa/bbbb"))); - Assertions.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/a%61%2fa%61/bbbb"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "aaaa/bbbb"))); - Assertions.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "aaaa"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "aaaa/bbbb"))); - Assertions.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/"))); - Assertions.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa"))); - Assertions.assertEquals("/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", ""))); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("/aaaa/bbbb")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("/aaaa/")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("/aaaa/../aaaa/")); + Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix("/aaaa/bbbb/cccc")); + Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix("/aaaa/bbbb/")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("/aaaa/bbbb/..")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("/aaaa/bbbb?////")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("/aaaa//bbbb")); + Assertions.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix("/aa%2faa/bbbb")); + Assertions.assertEquals("/aa%2Faa/", RequestSupport.extractPathPrefix("/a%61%2fa%61/bbbb")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("aaaa/bbbb")); + Assertions.assertEquals("/", RequestSupport.extractPathPrefix("aaaa")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("aaaa/bbbb")); + Assertions.assertEquals("/", RequestSupport.extractPathPrefix("/")); + Assertions.assertEquals("/", RequestSupport.extractPathPrefix("/aaaa")); + Assertions.assertEquals("/", RequestSupport.extractPathPrefix("")); } @Test void testPathPrefixExtractionDots() { - Assertions.assertEquals("/aaaa/b.b/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/b.b/cccc"))); - Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/cc.cc"))); - Assertions.assertEquals("/a.a/b..b/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/a.a/b..b/c."))); - Assertions.assertEquals("/aaaa/.bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/.bbbb/cccc"))); - Assertions.assertEquals("/aaaa/.../", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/.../cccc"))); - Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/./bbbb/cccc"))); - Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/bbbb/."))); - Assertions.assertEquals("/cccc/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "/aaaa/%2E%2E/cccc/dddd"))); - Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "../aaaa/bbbb"))); - Assertions.assertEquals("/foo.bar/", RequestSupport.extractPathPrefix(new BasicHttpRequest("GET", "foo.bar/baz"))); + Assertions.assertEquals("/aaaa/b.b/", RequestSupport.extractPathPrefix("/aaaa/b.b/cccc")); + Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix("/aaaa/bbbb/cc.cc")); + Assertions.assertEquals("/a.a/b..b/", RequestSupport.extractPathPrefix("/a.a/b..b/c.")); + Assertions.assertEquals("/aaaa/.bbbb/", RequestSupport.extractPathPrefix("/aaaa/.bbbb/cccc")); + Assertions.assertEquals("/aaaa/.../", RequestSupport.extractPathPrefix("/aaaa/.../cccc")); + Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix("/aaaa/./bbbb/cccc")); + Assertions.assertEquals("/aaaa/bbbb/", RequestSupport.extractPathPrefix("/aaaa/bbbb/.")); + Assertions.assertEquals("/cccc/", RequestSupport.extractPathPrefix("/aaaa/%2E%2E/cccc/dddd")); + Assertions.assertEquals("/aaaa/", RequestSupport.extractPathPrefix("../aaaa/bbbb")); + Assertions.assertEquals("/foo.bar/", RequestSupport.extractPathPrefix("foo.bar/baz")); + } + + @Test + void testPathPrefixExtractor() { + final Supplier pathPrefixExtractor = RequestSupport.pathPrefixSupplier("/aaaa/bbbb"); + final String pathPrefix = pathPrefixExtractor.get(); + Assertions.assertEquals("/aaaa/", pathPrefix); + Assertions.assertSame(pathPrefix, pathPrefixExtractor.get()); + Assertions.assertSame(pathPrefix, pathPrefixExtractor.get()); } } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestAsyncProtocolExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestAsyncProtocolExec.java index 7fcae912dc..8d70ab9180 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestAsyncProtocolExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/TestAsyncProtocolExec.java @@ -30,6 +30,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.apache.hc.client5.http.AuthenticationStrategy; @@ -38,15 +39,22 @@ import org.apache.hc.client5.http.async.AsyncExecCallback; import org.apache.hc.client5.http.async.AsyncExecChain; import org.apache.hc.client5.http.async.AsyncExecRuntime; +import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.RequestSupport; import org.apache.hc.client5.http.impl.auth.BasicAuthCache; import org.apache.hc.client5.http.impl.auth.BasicScheme; import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.concurrent.CancellableDependency; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.function.Supplier; +import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.EntityDetails; import org.apache.hc.core5.http.HttpHeaders; import org.apache.hc.core5.http.HttpHost; @@ -60,6 +68,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -72,19 +82,188 @@ class TestAsyncProtocolExec { @Mock private AuthenticationStrategy proxyAuthStrategy; @Mock + private AuthCache authCache; + @Mock private AsyncExecChain chain; @Mock private AsyncExecRuntime execRuntime; + private AtomicBoolean pathPrefixExtracted; + private Factory> prefixExtractorFactory; private AsyncProtocolExec protocolExec; private HttpHost target; @BeforeEach void setup() { - protocolExec = new AsyncProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, true); + pathPrefixExtracted = new AtomicBoolean(); + prefixExtractorFactory = request -> () -> { + pathPrefixExtracted.set(true); + return RequestSupport.extractPathPrefix(request.getPath()); + }; + protocolExec = new AsyncProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory, false); target = new HttpHost("http", "foo", 80); } + @Test + void testNoAuthChallengeNoAuthCache() throws Exception { + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(null); + + final AsyncExecChain.Scope scope = new AsyncExecChain.Scope("test", route, request, + Mockito.mock(CancellableDependency.class), context, execRuntime, null, new AtomicInteger(1)); + + final AtomicInteger proceedCount = new AtomicInteger(0); + final List requests = new ArrayList<>(); + stubChain(proceedCount, requests, 200); + + final RecordingCallback callback = new RecordingCallback(); + + protocolExec.execute(request, (AsyncEntityProducer) null, scope, chain, callback); + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange.getState()); + Assertions.assertNull(authExchange.getPathPrefix()); + + final AuthCache authCache = context.getAuthCache(); + Assertions.assertNull(authCache); + Assertions.assertFalse(pathPrefixExtracted.get()); + } + + @ParameterizedTest(name = "{index}: auth caching enabled: {0}") + @CsvSource({"true", "false"}) + void testNoAuthChallenge(final boolean authCacheEnabled) throws Exception { + if (!authCacheEnabled) { + protocolExec = new AsyncProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory, true); + } + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + + final AsyncExecChain.Scope scope = new AsyncExecChain.Scope("test", route, request, + Mockito.mock(CancellableDependency.class), context, execRuntime, null, new AtomicInteger(1)); + + final AtomicInteger proceedCount = new AtomicInteger(0); + final List requests = new ArrayList<>(); + stubChain(proceedCount, requests, 200); + + final RecordingCallback callback = new RecordingCallback(); + + protocolExec.execute(request, (AsyncEntityProducer) null, scope, chain, callback); + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange.getState()); + Assertions.assertNull(authExchange.getPathPrefix()); + + if (authCacheEnabled) { + Mockito.verify(authCache).get(target, "/aaaa/"); + Mockito.verify(authCache).get(target, null); + Mockito.verifyNoMoreInteractions(authCache); + } else { + Mockito.verifyNoInteractions(authCache); + Assertions.assertFalse(pathPrefixExtracted.get()); + } + } + + @ParameterizedTest(name = "{index}: auth caching enabled: {0}") + @CsvSource({"true", "false"}) + void testAuthChallengeFailure(final boolean authCacheEnabled) throws Exception { + if (!authCacheEnabled) { + protocolExec = new AsyncProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory,true); + } + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + context.setCredentialsProvider(CredentialsProviderBuilder.create() + .add(target, new UsernamePasswordCredentials("test", "banana".toCharArray())) + .build()); + + final AsyncExecChain.Scope scope = new AsyncExecChain.Scope("test", route, request, + Mockito.mock(CancellableDependency.class), context, execRuntime, null, new AtomicInteger(1)); + + final AtomicInteger proceedCount = new AtomicInteger(0); + final List requests = new ArrayList<>(); + stubChain(proceedCount, requests, 401); + + final BasicScheme basicScheme = new BasicScheme(); + Mockito.when(targetAuthStrategy.select( + Mockito.eq(ChallengeType.TARGET), + Mockito.any(), + Mockito.any())).thenReturn(Collections.singletonList(basicScheme)); + + final RecordingCallback callback = new RecordingCallback(); + + protocolExec.execute(request, (AsyncEntityProducer) null, scope, chain, callback); + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.FAILURE, authExchange.getState()); + Assertions.assertEquals("/aaaa/", authExchange.getPathPrefix()); + + if (authCacheEnabled) { + Mockito.verify(authCache).get(target, "/aaaa/"); + Mockito.verify(authCache).get(target, null); + Mockito.verify(authCache, Mockito.atLeastOnce()).remove(target, "/aaaa/"); + Mockito.verifyNoMoreInteractions(authCache); + } else { + Mockito.verifyNoInteractions(authCache); + Assertions.assertTrue(pathPrefixExtracted.get()); + } + } + + @ParameterizedTest(name = "{index}: auth caching enabled: {0}") + @CsvSource({"true", "false"}) + void testAuthChallengeSuccess(final boolean authCacheEnabled) throws Exception { + if (!authCacheEnabled) { + protocolExec = new AsyncProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory,true); + } + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + context.setCredentialsProvider(CredentialsProviderBuilder.create() + .add(target, new UsernamePasswordCredentials("test", "test".toCharArray())) + .build()); + + final AsyncExecChain.Scope scope = new AsyncExecChain.Scope("test", route, request, + Mockito.mock(CancellableDependency.class), context, execRuntime, null, new AtomicInteger(1)); + + final AtomicInteger proceedCount = new AtomicInteger(0); + final List requests = new ArrayList<>(); + stubChain(proceedCount, requests, 401, 200); + + final BasicScheme basicScheme = new BasicScheme(); + Mockito.when(targetAuthStrategy.select( + Mockito.eq(ChallengeType.TARGET), + Mockito.any(), + Mockito.any())).thenReturn(Collections.singletonList(basicScheme)); + + final RecordingCallback callback = new RecordingCallback(); + + protocolExec.execute(request, (AsyncEntityProducer) null, scope, chain, callback); + + if (authCacheEnabled) { + Mockito.verify(authCache).get(target, "/aaaa/"); + Mockito.verify(authCache).get(target, null); + Mockito.verify(authCache, Mockito.atLeastOnce()).remove(target, "/aaaa/"); + Mockito.verify(authCache).put(target, "/aaaa/", basicScheme); + Mockito.verifyNoMoreInteractions(authCache); + } else { + Mockito.verifyNoInteractions(authCache); + Assertions.assertTrue(pathPrefixExtracted.get()); + } + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.SUCCESS, authExchange.getState()); + Assertions.assertEquals("/aaaa/", authExchange.getPathPrefix()); + } + @Test void testAuthExchangePathPrefixRestoredAfterChallenge() throws Exception { final HttpRoute route = new HttpRoute(target); @@ -196,6 +375,9 @@ private void stubChain(final AtomicInteger proceedCount, final List final AsyncExecCallback cb = invocation.getArgument(3, AsyncExecCallback.class); final int code = codes[Math.min(proceedCount.getAndIncrement(), codes.length - 1)]; final HttpResponse resp = new BasicHttpResponse(code, "Huh?"); + if (code == 401) { + resp.setHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=test"); + } if (code == 407) { resp.setHeader(HttpHeaders.PROXY_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=test"); } @@ -268,4 +450,5 @@ void testProxyChallengeOnPlainRouteStillAuthenticates() throws Exception { Assertions.assertEquals(Collections.singletonList(200), callback.codes); Assertions.assertNotNull(requests.get(1).getFirstHeader(HttpHeaders.PROXY_AUTHORIZATION)); } + } diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java index 9588a42972..6317a573bb 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestProtocolExec.java @@ -31,35 +31,45 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collections; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.RouteInfo; +import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.StandardAuthScheme; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; import org.apache.hc.client5.http.classic.ExecChain; import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.entity.EntityBuilder; +import org.apache.hc.client5.http.impl.RequestSupport; import org.apache.hc.client5.http.impl.auth.BasicAuthCache; import org.apache.hc.client5.http.impl.auth.BasicScheme; import org.apache.hc.client5.http.impl.auth.CredentialsProviderBuilder; import org.apache.hc.client5.http.protocol.HttpClientContext; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.function.Supplier; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.ClassicHttpResponse; 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.ProtocolException; +import org.apache.hc.core5.http.io.support.ClassicResponseBuilder; import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.Mockito; @@ -75,19 +85,191 @@ class TestProtocolExec { @Mock private AuthenticationStrategy proxyAuthStrategy; @Mock + private AuthCache authCache; + @Mock private ExecChain chain; @Mock private ExecRuntime execRuntime; + private AtomicBoolean pathPrefixExtracted; + private Factory> prefixExtractorFactory; private ProtocolExec protocolExec; private HttpHost target; @BeforeEach void setup() { - protocolExec = new ProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, true); + pathPrefixExtracted = new AtomicBoolean(); + prefixExtractorFactory = request -> () -> { + pathPrefixExtracted.set(true); + return RequestSupport.extractPathPrefix(request.getPath()); + }; + protocolExec = new ProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory, false); target = new HttpHost("foo", 80); } + @Test + void testNoAuthChallengeNoAuthCache() throws Exception { + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(null); + + final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); + + Mockito.when(chain.proceed( + Mockito.same(request), + Mockito.any())).thenReturn(new BasicClassicHttpResponse(200)); + + final ClassicHttpResponse finalResponse = protocolExec.execute(request, scope, chain); + + Assertions.assertNotNull(finalResponse); + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange.getState()); + Assertions.assertNull(authExchange.getPathPrefix()); + + final AuthCache authCache = context.getAuthCache(); + Assertions.assertNull(authCache); + Assertions.assertFalse(pathPrefixExtracted.get()); + } + + @ParameterizedTest(name = "{index}: auth caching enabled: {0}") + @CsvSource({"true", "false"}) + void testNoAuthChallenge(final boolean authCacheEnabled) throws Exception { + if (!authCacheEnabled) { + protocolExec = new ProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory, true); + } + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + + final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); + + Mockito.when(chain.proceed( + Mockito.same(request), + Mockito.any())).thenReturn(new BasicClassicHttpResponse(200)); + + final ClassicHttpResponse finalResponse = protocolExec.execute(request, scope, chain); + + Assertions.assertNotNull(finalResponse); + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.UNCHALLENGED, authExchange.getState()); + Assertions.assertNull(authExchange.getPathPrefix()); + + if (authCacheEnabled) { + Mockito.verify(authCache).get(target, "/aaaa/"); + Mockito.verify(authCache).get(target, null); + Mockito.verifyNoMoreInteractions(authCache); + } else { + Mockito.verifyNoInteractions(authCache); + Assertions.assertFalse(pathPrefixExtracted.get()); + } + } + + @ParameterizedTest(name = "{index}: auth caching enabled: {0}") + @CsvSource({"true", "false"}) + void testAuthChallengeFailure(final boolean authCacheEnabled) throws Exception { + if (!authCacheEnabled) { + protocolExec = new ProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory,true); + } + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + context.setCredentialsProvider(CredentialsProviderBuilder.create() + .add(target, new UsernamePasswordCredentials("test", "banana".toCharArray())) + .build()); + + final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); + + Mockito.when(chain.proceed( + Mockito.same(request), + Mockito.any())).thenReturn(ClassicResponseBuilder.create(401) + .addHeader(HttpHeaders.WWW_AUTHENTICATE, "BASIC realm=test") + .build()); + + final BasicScheme basicScheme = new BasicScheme(); + Mockito.when(targetAuthStrategy.select( + Mockito.eq(ChallengeType.TARGET), + Mockito.any(), + Mockito.any())).thenReturn(Collections.singletonList(basicScheme)); + + final ClassicHttpResponse finalResponse = protocolExec.execute(request, scope, chain); + + Assertions.assertNotNull(finalResponse); + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.FAILURE, authExchange.getState()); + Assertions.assertEquals("/aaaa/", authExchange.getPathPrefix()); + + if (authCacheEnabled) { + Mockito.verify(authCache).get(target, "/aaaa/"); + Mockito.verify(authCache).get(target, null); + Mockito.verify(authCache, Mockito.atLeastOnce()).remove(target, "/aaaa/"); + Mockito.verifyNoMoreInteractions(authCache); + } else { + Mockito.verifyNoInteractions(authCache); + Assertions.assertTrue(pathPrefixExtracted.get()); + } + } + + @ParameterizedTest(name = "{index}: auth caching enabled: {0}") + @CsvSource({"true", "false"}) + void testAuthChallengeSuccess(final boolean authCacheEnabled) throws Exception { + if (!authCacheEnabled) { + protocolExec = new ProtocolExec(targetAuthStrategy, proxyAuthStrategy, null, prefixExtractorFactory,true); + } + final HttpRoute route = new HttpRoute(target); + final ClassicHttpRequest request = new HttpGet("/aaaa/bbbb"); + final HttpClientContext context = HttpClientContext.create(); + context.setAuthCache(authCache); + context.setCredentialsProvider(CredentialsProviderBuilder.create() + .add(target, new UsernamePasswordCredentials("test", "test".toCharArray())) + .build()); + + final ExecChain.Scope scope = new ExecChain.Scope("test", route, request, execRuntime, context); + + Mockito.when(chain.proceed( + Mockito.same(request), + Mockito.any())).thenReturn( + ClassicResponseBuilder.create(401) + .addHeader(HttpHeaders.WWW_AUTHENTICATE, "BASIC realm=test") + .build(), + ClassicResponseBuilder.create(200) + .build()); + + final BasicScheme basicScheme = new BasicScheme(); + Mockito.when(targetAuthStrategy.select( + Mockito.eq(ChallengeType.TARGET), + Mockito.any(), + Mockito.any())).thenReturn(Collections.singletonList(basicScheme)); + + final ClassicHttpResponse finalResponse = protocolExec.execute(request, scope, chain); + + Assertions.assertNotNull(finalResponse); + + if (authCacheEnabled) { + Mockito.verify(authCache).get(target, "/aaaa/"); + Mockito.verify(authCache).get(target, null); + Mockito.verify(authCache, Mockito.atLeastOnce()).remove(target, "/aaaa/"); + Mockito.verify(authCache).put(target, "/aaaa/", basicScheme); + Mockito.verifyNoMoreInteractions(authCache); + } else { + Mockito.verifyNoInteractions(authCache); + Assertions.assertTrue(pathPrefixExtracted.get()); + } + + final AuthExchange authExchange = context.getAuthExchange(target); + Assertions.assertNotNull(authExchange); + Assertions.assertEquals(AuthExchange.State.SUCCESS, authExchange.getState()); + Assertions.assertEquals("/aaaa/", authExchange.getPathPrefix()); + } + @Test void testUserInfoInRequestURI() { final HttpRoute route = new HttpRoute(new HttpHost("somehost", 8080)); @@ -399,4 +581,5 @@ void testProxyChallengeOnPlainRouteStillAuthenticates() throws Exception { Mockito.verify(chain, Mockito.times(2)).proceed(captor.capture(), Mockito.same(scope)); Assertions.assertNotNull(captor.getAllValues().get(1).getFirstHeader(HttpHeaders.PROXY_AUTHORIZATION)); } + }