From 4150ac0592e29b885dce51d602cdddec853b4c13 Mon Sep 17 00:00:00 2001 From: Oleg Kalnichevski Date: Sat, 25 Sep 2021 18:52:51 +0200 Subject: [PATCH] Refactored AuthCache keeping logic into a separate utility class --- .../http/impl/async/AsyncConnectExec.java | 33 ++++- .../http/impl/async/AsyncProtocolExec.java | 82 +++++++--- .../http/impl/async/H2AsyncClientBuilder.java | 15 +- .../impl/async/HttpAsyncClientBuilder.java | 15 +- .../http/impl/auth/AuthCacheKeeper.java | 140 ++++++++++++++++++ .../http/impl/auth/HttpAuthenticator.java | 52 +------ .../http/impl/classic/ConnectExec.java | 43 ++++-- .../http/impl/classic/HttpClientBuilder.java | 15 +- .../http/impl/classic/ProtocolExec.java | 63 ++++++-- .../http/protocol/RequestAuthCache.java | 21 +-- .../http/impl/auth/TestHttpAuthenticator.java | 13 -- .../http/impl/classic/TestConnectExec.java | 2 +- .../http/impl/classic/TestProtocolExec.java | 2 +- 13 files changed, 362 insertions(+), 134 deletions(-) create mode 100644 httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java 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 b0288c75c..4f83962ee 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 @@ -33,6 +33,7 @@ import java.io.InterruptedIOException; import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.RouteTracker; +import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.async.AsyncExecCallback; import org.apache.hc.client5.http.async.AsyncExecChain; import org.apache.hc.client5.http.async.AsyncExecChainHandler; @@ -41,6 +42,7 @@ 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.TunnelRefusedException; +import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; 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.HttpClientContext; @@ -84,17 +86,21 @@ public final class AsyncConnectExec implements AsyncExecChainHandler { private final HttpProcessor proxyHttpProcessor; private final AuthenticationStrategy proxyAuthStrategy; private final HttpAuthenticator authenticator; + private final AuthCacheKeeper authCacheKeeper; private final HttpRouteDirector routeDirector; public AsyncConnectExec( final HttpProcessor proxyHttpProcessor, - final AuthenticationStrategy proxyAuthStrategy) { + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled) { Args.notNull(proxyHttpProcessor, "Proxy HTTP processor"); Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); this.proxyHttpProcessor = proxyHttpProcessor; this.proxyAuthStrategy = proxyAuthStrategy; - this.authenticator = new HttpAuthenticator(LOG); - this.routeDirector = new BasicRouteDirector(); + this.authenticator = new HttpAuthenticator(); + this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver); + this.routeDirector = new BasicRouteDirector(); } static class State { @@ -372,6 +378,10 @@ public final class AsyncConnectExec implements AsyncExecChainHandler { final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); + if (authCacheKeeper != null) { + authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext); + } + final HttpRequest connect = new BasicHttpRequest(Method.CONNECT, nextHop, nextHop.toHostString()); connect.setVersion(HttpVersion.HTTP_1_1); @@ -431,9 +441,24 @@ public final class AsyncConnectExec implements AsyncExecChainHandler { final RequestConfig config = context.getRequestConfig(); if (config.isAuthenticationEnabled()) { final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + if (proxyAuthRequested) { + authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + } + } + if (proxyAuthRequested) { - return authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + } + + return updated; } } return false; 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 880d9e5f8..4fe36378b 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 @@ -32,6 +32,7 @@ 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.SchemePortResolver; import org.apache.hc.client5.http.async.AsyncExecCallback; import org.apache.hc.client5.http.async.AsyncExecChain; import org.apache.hc.client5.http.async.AsyncExecChainHandler; @@ -42,6 +43,7 @@ import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.CredentialsStore; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.AuthSupport; +import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; @@ -87,15 +89,19 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { private final AuthenticationStrategy targetAuthStrategy; private final AuthenticationStrategy proxyAuthStrategy; private final HttpAuthenticator authenticator; + private final AuthCacheKeeper authCacheKeeper; AsyncProtocolExec( final HttpProcessor httpProcessor, final AuthenticationStrategy targetAuthStrategy, - final AuthenticationStrategy proxyAuthStrategy) { + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled) { 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(LOG); + this.authenticator = new HttpAuthenticator(); + this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver); } @Override @@ -141,11 +147,26 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { AuthSupport.extractFromAuthority(request.getScheme(), authority, (CredentialsStore) credsProvider); } + final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority()); + final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target); + final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); + + if (authCacheKeeper != null) { + authCacheKeeper.loadPreemptively(target, targetAuthExchange, clientContext); + if (proxy != null) { + authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, clientContext); + } + } + final AtomicBoolean challenged = new AtomicBoolean(false); - internalExecute(challenged, request, entityProducer, scope, chain, asyncExecCallback); + internalExecute(target, targetAuthExchange, proxyAuthExchange, + challenged, request, entityProducer, scope, chain, asyncExecCallback); } private void internalExecute( + final HttpHost target, + final AuthExchange targetAuthExchange, + final AuthExchange proxyAuthExchange, final AtomicBoolean challenged, final HttpRequest request, final AsyncEntityProducer entityProducer, @@ -158,10 +179,6 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { final AsyncExecRuntime execRuntime = scope.execRuntime; final HttpHost proxy = route.getProxyHost(); - final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority()); - - final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target); - final AuthExchange proxyAuthExchange = proxy != null ? clientContext.getAuthExchange(proxy) : new AuthExchange(); clientContext.setAttribute(HttpClientContext.HTTP_ROUTE, route); clientContext.setAttribute(HttpCoreContext.HTTP_REQUEST, request); @@ -194,7 +211,13 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { // Do not perform authentication for TRACE request return asyncExecCallback.handleResponse(response, entityDetails); } - if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, clientContext)) { + if (needAuthentication( + targetAuthExchange, + proxyAuthExchange, + proxy != null ? proxy : target, + target, + response, + clientContext)) { challenged.set(true); return null; } @@ -244,7 +267,8 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { if (entityProducer != null) { entityProducer.releaseResources(); } - internalExecute(challenged, request, entityProducer, scope, chain, asyncExecCallback); + internalExecute(target, targetAuthExchange, proxyAuthExchange, + challenged, request, entityProducer, scope, chain, asyncExecCallback); } catch (final HttpException | IOException ex) { asyncExecCallback.failed(ex); } @@ -272,31 +296,53 @@ public final class AsyncProtocolExec implements AsyncExecChainHandler { private boolean needAuthentication( final AuthExchange targetAuthExchange, final AuthExchange proxyAuthExchange, - final HttpRoute route, - final HttpRequest request, + final HttpHost proxy, + final HttpHost target, final HttpResponse response, final HttpClientContext context) { final RequestConfig config = context.getRequestConfig(); if (config.isAuthenticationEnabled()) { - final HttpHost target = AuthSupport.resolveAuthTarget(request, route); 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(); + if (authCacheKeeper != null) { + if (targetAuthRequested) { + authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context); + } } + final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + if (authCacheKeeper != null) { + if (proxyAuthRequested) { + authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + } + } + if (targetAuthRequested) { - return authenticator.updateAuthState(target, ChallengeType.TARGET, response, + final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(target, targetAuthExchange, context); + } + + return updated; } if (proxyAuthRequested) { - return authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + } + + return updated; } } return false; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java index 494e925c3..098acddee 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/H2AsyncClientBuilder.java @@ -67,7 +67,6 @@ import org.apache.hc.client5.http.impl.nio.MultihomeConnectionInitiator; import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner; import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.protocol.RequestAddCookies; -import org.apache.hc.client5.http.protocol.RequestAuthCache; import org.apache.hc.client5.http.protocol.RequestDefaultHeaders; import org.apache.hc.client5.http.protocol.RequestExpectContinue; import org.apache.hc.client5.http.protocol.ResponseProcessCookies; @@ -638,7 +637,9 @@ public class H2AsyncClientBuilder { execChainDefinition.addFirst( new AsyncConnectExec( new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)), - proxyAuthStrategyCopy), + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled), ChainElement.CONNECT.name()); final HttpProcessorBuilder b = HttpProcessorBuilder.create(); @@ -663,9 +664,6 @@ public class H2AsyncClientBuilder { if (!cookieManagementDisabled) { b.add(new RequestAddCookies()); } - if (!authCachingDisabled) { - b.add(new RequestAuthCache()); - } if (!cookieManagementDisabled) { b.add(new ResponseProcessCookies()); } @@ -686,7 +684,12 @@ public class H2AsyncClientBuilder { final HttpProcessor httpProcessor = b.build(); execChainDefinition.addFirst( - new AsyncProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy), + new AsyncProtocolExec( + httpProcessor, + targetAuthStrategyCopy, + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled), ChainElement.PROTOCOL.name()); // Add request retry executor, if not disabled diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java index 44a675908..6127ce08e 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java @@ -75,7 +75,6 @@ import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; import org.apache.hc.client5.http.nio.AsyncClientConnectionManager; import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.protocol.RequestAddCookies; -import org.apache.hc.client5.http.protocol.RequestAuthCache; import org.apache.hc.client5.http.protocol.RequestDefaultHeaders; import org.apache.hc.client5.http.protocol.RequestExpectContinue; import org.apache.hc.client5.http.protocol.ResponseProcessCookies; @@ -778,7 +777,9 @@ public class HttpAsyncClientBuilder { execChainDefinition.addFirst( new AsyncConnectExec( new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)), - proxyAuthStrategyCopy), + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled), ChainElement.CONNECT.name()); final HttpProcessorBuilder b = HttpProcessorBuilder.create(); @@ -803,9 +804,6 @@ public class HttpAsyncClientBuilder { if (!cookieManagementDisabled) { b.add(new RequestAddCookies()); } - if (!authCachingDisabled) { - b.add(new RequestAuthCache()); - } if (!cookieManagementDisabled) { b.add(new ResponseProcessCookies()); } @@ -826,7 +824,12 @@ public class HttpAsyncClientBuilder { final HttpProcessor httpProcessor = b.build(); execChainDefinition.addFirst( - new AsyncProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy), + new AsyncProtocolExec( + httpProcessor, + targetAuthStrategyCopy, + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled), ChainElement.PROTOCOL.name()); // Add request retry executor, if not disabled 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 new file mode 100644 index 000000000..01f959a8d --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/AuthCacheKeeper.java @@ -0,0 +1,140 @@ +/* + * ==================================================================== + * 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 + * . + * + */ + +package org.apache.hc.client5.http.impl.auth; + +import org.apache.hc.client5.http.SchemePortResolver; +import org.apache.hc.client5.http.auth.AuthCache; +import org.apache.hc.client5.http.auth.AuthExchange; +import org.apache.hc.client5.http.auth.AuthScheme; +import org.apache.hc.client5.http.auth.AuthStateCacheable; +import org.apache.hc.client5.http.protocol.HttpClientContext; +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.http.HttpHost; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Utility class that implements commons aspects of the client side authentication cache keeping. + * + * @since 5.2 + */ +@Internal +@Contract(threading = ThreadingBehavior.STATELESS) +public final class AuthCacheKeeper { + + private static final Logger LOG = LoggerFactory.getLogger(AuthCacheKeeper.class); + + private final SchemePortResolver schemePortResolver; + + public AuthCacheKeeper(final SchemePortResolver schemePortResolver) { + this.schemePortResolver = schemePortResolver; + } + + public void updateOnChallenge(final HttpHost host, + final AuthExchange authExchange, + final HttpContext context) { + clearCache(host, HttpClientContext.adapt(context)); + } + + public void updateOnNoChallenge(final HttpHost host, + final AuthExchange authExchange, + final HttpContext context) { + if (authExchange.getState() == AuthExchange.State.SUCCESS) { + updateCache(host, authExchange.getAuthScheme(), HttpClientContext.adapt(context)); + } + } + + public void updateOnResponse(final HttpHost host, + final AuthExchange authExchange, + final HttpContext context) { + if (authExchange.getState() == AuthExchange.State.FAILURE) { + clearCache(host, HttpClientContext.adapt(context)); + } + } + + public void loadPreemptively(final HttpHost host, + final AuthExchange authExchange, + final HttpContext context) { + if (authExchange.getState() == AuthExchange.State.UNCHALLENGED) { + final AuthScheme authScheme = loadFromCache(host, HttpClientContext.adapt(context)); + if (authScheme != null) { + authExchange.select(authScheme); + } + } + } + + private AuthScheme loadFromCache(final HttpHost host, + final HttpClientContext clientContext) { + final AuthCache authCache = clientContext.getAuthCache(); + if (authCache != null) { + final AuthScheme authScheme = authCache.get(host); + if (authScheme != null) { + if (LOG.isDebugEnabled()) { + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Re-using cached '{}' auth scheme for {}", exchangeId, authScheme.getName(), host); + } + return authScheme; + } + } + return null; + } + + private void updateCache(final HttpHost host, + final AuthScheme authScheme, + final HttpClientContext clientContext) { + final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null; + if (cacheable) { + AuthCache authCache = clientContext.getAuthCache(); + if (authCache == null) { + authCache = new BasicAuthCache(schemePortResolver); + clientContext.setAuthCache(authCache); + } + if (LOG.isDebugEnabled()) { + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Caching '{}' auth scheme for {}", exchangeId, authScheme.getName(), host); + } + authCache.put(host, authScheme); + } + } + + private void clearCache(final HttpHost host, + final HttpClientContext clientContext) { + final AuthCache authCache = clientContext.getAuthCache(); + if (authCache != null) { + if (LOG.isDebugEnabled()) { + final String exchangeId = clientContext.getExchangeId(); + LOG.debug("{} Clearing cached auth scheme for {}", exchangeId, host); + } + authCache.remove(host); + } + } + +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java index 7987402a3..0a5592b49 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/auth/HttpAuthenticator.java @@ -35,18 +35,15 @@ import java.util.Map; import java.util.Queue; import org.apache.hc.client5.http.AuthenticationStrategy; -import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthChallenge; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScheme; -import org.apache.hc.client5.http.auth.AuthStateCacheable; import org.apache.hc.client5.http.auth.AuthenticationException; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.CredentialsProvider; import org.apache.hc.client5.http.auth.MalformedChallengeException; import org.apache.hc.client5.http.protocol.HttpClientContext; 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.http.FormattedHeader; import org.apache.hc.core5.http.Header; @@ -66,6 +63,9 @@ import org.slf4j.LoggerFactory; /** * Utility class that implements commons aspects of the client side HTTP authentication. + *

+ * Please note that since version 5.2 this class no longer updated the authentication cache + * bound to the execution context. * * @since 4.3 */ @@ -77,15 +77,9 @@ public final class HttpAuthenticator { private final Logger log; private final AuthChallengeParser parser; - @Internal - public HttpAuthenticator(final Logger log) { - super(); - this.log = log != null ? log : DEFAULT_LOGGER; - this.parser = new AuthChallengeParser(); - } - public HttpAuthenticator() { - this(null); + this.log = DEFAULT_LOGGER; + this.parser = new AuthChallengeParser(); } /** @@ -124,9 +118,6 @@ public final class HttpAuthenticator { if (log.isDebugEnabled()) { log.debug("{} Authentication required", exchangeId); } - if (authExchange.getState() == AuthExchange.State.SUCCESS) { - clearCache(host, clientContext); - } return true; } switch (authExchange.getState()) { @@ -136,7 +127,6 @@ public final class HttpAuthenticator { log.debug("{} Authentication succeeded", exchangeId); } authExchange.setState(AuthExchange.State.SUCCESS); - updateCache(host, authExchange.getAuthScheme(), clientContext); break; case SUCCESS: break; @@ -213,7 +203,6 @@ public final class HttpAuthenticator { if (log.isDebugEnabled()) { log.debug("{} Response contains no valid authentication challenges", exchangeId); } - clearCache(host, clientContext); authExchange.reset(); return false; } @@ -242,15 +231,14 @@ public final class HttpAuthenticator { if (log.isWarnEnabled()) { log.warn("{} {}", exchangeId, ex.getMessage()); } - clearCache(host, clientContext); authExchange.reset(); + authExchange.setState(AuthExchange.State.FAILURE); return false; } if (authScheme.isChallengeComplete()) { if (log.isDebugEnabled()) { log.debug("{} Authentication failed", exchangeId); } - clearCache(host, clientContext); authExchange.reset(); authExchange.setState(AuthExchange.State.FAILURE); return false; @@ -376,32 +364,4 @@ public final class HttpAuthenticator { } } - private void updateCache(final HttpHost host, final AuthScheme authScheme, final HttpClientContext clientContext) { - final boolean cacheable = authScheme.getClass().getAnnotation(AuthStateCacheable.class) != null; - if (cacheable) { - AuthCache authCache = clientContext.getAuthCache(); - if (authCache == null) { - authCache = new BasicAuthCache(); - clientContext.setAuthCache(authCache); - } - if (log.isDebugEnabled()) { - final String exchangeId = clientContext.getExchangeId(); - log.debug("{} Caching '{}' auth scheme for {}", exchangeId, authScheme.getName(), host); - } - authCache.put(host, authScheme); - } - } - - private void clearCache(final HttpHost host, final HttpClientContext clientContext) { - - final AuthCache authCache = clientContext.getAuthCache(); - if (authCache != null) { - if (log.isDebugEnabled()) { - final String exchangeId = clientContext.getExchangeId(); - log.debug("{} Clearing cached auth scheme for {}", exchangeId, host); - } - authCache.remove(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 357915fcb..5b80e21e8 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 @@ -32,6 +32,7 @@ import java.io.IOException; import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.HttpRoute; import org.apache.hc.client5.http.RouteTracker; +import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.classic.ExecChain; @@ -39,6 +40,7 @@ import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.TunnelRefusedException; +import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; 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.HttpClientContext; @@ -82,20 +84,24 @@ public final class ConnectExec implements ExecChainHandler { private final HttpProcessor proxyHttpProcessor; private final AuthenticationStrategy proxyAuthStrategy; private final HttpAuthenticator authenticator; + private final AuthCacheKeeper authCacheKeeper; private final HttpRouteDirector routeDirector; public ConnectExec( final ConnectionReuseStrategy reuseStrategy, final HttpProcessor proxyHttpProcessor, - final AuthenticationStrategy proxyAuthStrategy) { + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled) { Args.notNull(reuseStrategy, "Connection reuse strategy"); Args.notNull(proxyHttpProcessor, "Proxy HTTP processor"); Args.notNull(proxyAuthStrategy, "Proxy authentication strategy"); - this.reuseStrategy = reuseStrategy; + this.reuseStrategy = reuseStrategy; this.proxyHttpProcessor = proxyHttpProcessor; - this.proxyAuthStrategy = proxyAuthStrategy; - this.authenticator = new HttpAuthenticator(LOG); - this.routeDirector = new BasicRouteDirector(); + this.proxyAuthStrategy = proxyAuthStrategy; + this.authenticator = new HttpAuthenticator(); + this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver); + this.routeDirector = new BasicRouteDirector(); } @Override @@ -207,6 +213,11 @@ public final class ConnectExec implements ExecChainHandler { final HttpHost target = route.getTargetHost(); final HttpHost proxy = route.getProxyHost(); final AuthExchange proxyAuthExchange = context.getAuthExchange(proxy); + + if (authCacheKeeper != null) { + authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context); + } + ClassicHttpResponse response = null; final String authority = target.toHostString(); @@ -239,10 +250,24 @@ public final class ConnectExec implements ExecChainHandler { } if (config.isAuthenticationEnabled()) { - if (this.authenticator.isChallenged(proxy, ChallengeType.PROXY, response, - proxyAuthExchange, context)) { - if (this.authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, - this.proxyAuthStrategy, proxyAuthExchange, context)) { + final boolean proxyAuthRequested = authenticator.isChallenged(proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + if (proxyAuthRequested) { + authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + } + } + + if (proxyAuthRequested) { + final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + proxyAuthStrategy, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + } + if (updated) { // Retry request response = null; } diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java index 13e7b3bc2..d40d077c3 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/HttpClientBuilder.java @@ -77,7 +77,6 @@ import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.protocol.RedirectStrategy; import org.apache.hc.client5.http.protocol.RequestAddCookies; -import org.apache.hc.client5.http.protocol.RequestAuthCache; import org.apache.hc.client5.http.protocol.RequestClientConnControl; import org.apache.hc.client5.http.protocol.RequestDefaultHeaders; import org.apache.hc.client5.http.protocol.RequestExpectContinue; @@ -791,7 +790,9 @@ public class HttpClientBuilder { new ConnectExec( reuseStrategyCopy, new DefaultHttpProcessor(new RequestTargetHost(), new RequestUserAgent(userAgentCopy)), - proxyAuthStrategyCopy), + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled), ChainElement.CONNECT.name()); final HttpProcessorBuilder b = HttpProcessorBuilder.create(); @@ -819,9 +820,6 @@ public class HttpClientBuilder { if (!cookieManagementDisabled) { b.add(new RequestAddCookies()); } - if (!authCachingDisabled) { - b.add(new RequestAuthCache()); - } if (!cookieManagementDisabled) { b.add(new ResponseProcessCookies()); } @@ -841,7 +839,12 @@ public class HttpClientBuilder { } final HttpProcessor httpProcessor = b.build(); execChainDefinition.addFirst( - new ProtocolExec(httpProcessor, targetAuthStrategyCopy, proxyAuthStrategyCopy), + new ProtocolExec( + httpProcessor, + targetAuthStrategyCopy, + proxyAuthStrategyCopy, + schemePortResolver != null ? schemePortResolver : DefaultSchemePortResolver.INSTANCE, + authCachingDisabled), ChainElement.PROTOCOL.name()); // Add request retry executor, if not disabled 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 14fcbfb5c..556f7a6ed 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 @@ -32,6 +32,7 @@ import java.util.Iterator; import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.ChallengeType; import org.apache.hc.client5.http.auth.CredentialsProvider; @@ -41,6 +42,7 @@ import org.apache.hc.client5.http.classic.ExecChainHandler; import org.apache.hc.client5.http.classic.ExecRuntime; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.AuthSupport; +import org.apache.hc.client5.http.impl.auth.AuthCacheKeeper; import org.apache.hc.client5.http.impl.auth.HttpAuthenticator; import org.apache.hc.client5.http.protocol.HttpClientContext; import org.apache.hc.core5.annotation.Contract; @@ -86,15 +88,19 @@ public final class ProtocolExec implements ExecChainHandler { private final AuthenticationStrategy targetAuthStrategy; private final AuthenticationStrategy proxyAuthStrategy; private final HttpAuthenticator authenticator; + private final AuthCacheKeeper authCacheKeeper; public ProtocolExec( final HttpProcessor httpProcessor, final AuthenticationStrategy targetAuthStrategy, - final AuthenticationStrategy proxyAuthStrategy) { + final AuthenticationStrategy proxyAuthStrategy, + final SchemePortResolver schemePortResolver, + final boolean authCachingDisabled) { 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(); + this.authCacheKeeper = authCachingDisabled ? null : new AuthCacheKeeper(schemePortResolver); } @Override @@ -149,6 +155,13 @@ public final class ProtocolExec implements ExecChainHandler { final AuthExchange targetAuthExchange = context.getAuthExchange(target); final AuthExchange proxyAuthExchange = proxy != null ? context.getAuthExchange(proxy) : new AuthExchange(); + if (authCacheKeeper != null) { + authCacheKeeper.loadPreemptively(target, targetAuthExchange, context); + if (proxy != null) { + authCacheKeeper.loadPreemptively(proxy, proxyAuthExchange, context); + } + } + RequestEntityProxy.enhance(request); for (;;) { @@ -188,7 +201,13 @@ public final class ProtocolExec implements ExecChainHandler { } return response; } - if (needAuthentication(targetAuthExchange, proxyAuthExchange, route, request, response, context)) { + if (needAuthentication( + targetAuthExchange, + proxyAuthExchange, + proxy != null ? proxy : target, + target, + response, + context)) { // Make sure the response body is fully consumed, if present final HttpEntity responseEntity = response.getEntity(); if (execRuntime.isConnectionReusable()) { @@ -238,31 +257,53 @@ public final class ProtocolExec implements ExecChainHandler { private boolean needAuthentication( final AuthExchange targetAuthExchange, final AuthExchange proxyAuthExchange, - final HttpRoute route, - final ClassicHttpRequest request, + final HttpHost proxy, + final HttpHost target, final HttpResponse response, final HttpClientContext context) { final RequestConfig config = context.getRequestConfig(); if (config.isAuthenticationEnabled()) { - final HttpHost target = AuthSupport.resolveAuthTarget(request, route); 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(); + if (authCacheKeeper != null) { + if (targetAuthRequested) { + authCacheKeeper.updateOnChallenge(target, targetAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(target, targetAuthExchange, context); + } } + final boolean proxyAuthRequested = authenticator.isChallenged( proxy, ChallengeType.PROXY, response, proxyAuthExchange, context); + if (authCacheKeeper != null) { + if (proxyAuthRequested) { + authCacheKeeper.updateOnChallenge(proxy, proxyAuthExchange, context); + } else { + authCacheKeeper.updateOnNoChallenge(proxy, proxyAuthExchange, context); + } + } + if (targetAuthRequested) { - return authenticator.updateAuthState(target, ChallengeType.TARGET, response, + final boolean updated = authenticator.updateAuthState(target, ChallengeType.TARGET, response, targetAuthStrategy, targetAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(target, targetAuthExchange, context); + } + + return updated; } if (proxyAuthRequested) { - return authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, + final boolean updated = authenticator.updateAuthState(proxy, ChallengeType.PROXY, response, proxyAuthStrategy, proxyAuthExchange, context); + + if (authCacheKeeper != null) { + authCacheKeeper.updateOnResponse(proxy, proxyAuthExchange, context); + } + + return updated; } } return false; diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestAuthCache.java b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestAuthCache.java index 597bb21a0..608357daa 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestAuthCache.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/protocol/RequestAuthCache.java @@ -34,6 +34,7 @@ import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.impl.RequestSupport; import org.apache.hc.core5.annotation.Contract; import org.apache.hc.core5.annotation.ThreadingBehavior; import org.apache.hc.core5.http.EntityDetails; @@ -42,7 +43,6 @@ import org.apache.hc.core5.http.HttpHost; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpRequestInterceptor; import org.apache.hc.core5.http.protocol.HttpContext; -import org.apache.hc.core5.net.URIAuthority; import org.apache.hc.core5.util.Args; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,7 +53,10 @@ import org.slf4j.LoggerFactory; * {@link AuthCache} associated with the given target or proxy host. * * @since 4.1 + * + * @deprecated Do not use. */ +@Deprecated @Contract(threading = ThreadingBehavior.STATELESS) public class RequestAuthCache implements HttpRequestInterceptor { @@ -96,19 +99,11 @@ public class RequestAuthCache implements HttpRequestInterceptor { return; } - final URIAuthority authority = request.getAuthority(); - final HttpHost target; - if (authority != null) { - target = new HttpHost( - request.getScheme(), - authority.getHostName(), - authority.getPort() >= 0 ? authority.getPort() : route.getTargetHost().getPort()); - } else { - target = route.getTargetHost(); - } + final HttpHost target = new HttpHost(request.getScheme(), request.getAuthority()); final AuthExchange targetAuthExchange = clientContext.getAuthExchange(target); if (targetAuthExchange.getState() == AuthExchange.State.UNCHALLENGED) { - final AuthScheme authScheme = authCache.get(target); + final String pathPrefix = RequestSupport.extractPathPrefix(request); + final AuthScheme authScheme = authCache.get(target, pathPrefix); if (authScheme != null) { if (LOG.isDebugEnabled()) { LOG.debug("{} Re-using cached '{}' auth scheme for {}", exchangeId, authScheme.getName(), target); @@ -121,7 +116,7 @@ public class RequestAuthCache implements HttpRequestInterceptor { if (proxy != null) { final AuthExchange proxyAuthExchange = clientContext.getAuthExchange(proxy); if (proxyAuthExchange.getState() == AuthExchange.State.UNCHALLENGED) { - final AuthScheme authScheme = authCache.get(proxy); + final AuthScheme authScheme = authCache.get(proxy, null); if (authScheme != null) { if (LOG.isDebugEnabled()) { LOG.debug("{} Re-using cached '{}' auth scheme for {}", exchangeId, authScheme.getName(), proxy); diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java index fd2489446..10e6946e8 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/auth/TestHttpAuthenticator.java @@ -29,7 +29,6 @@ package org.apache.hc.client5.http.impl.auth; import java.util.LinkedList; import java.util.Queue; -import org.apache.hc.client5.http.auth.AuthCache; import org.apache.hc.client5.http.auth.AuthExchange; import org.apache.hc.client5.http.auth.AuthScheme; import org.apache.hc.client5.http.auth.AuthSchemeFactory; @@ -80,7 +79,6 @@ public class TestHttpAuthenticator { private HttpHost defaultHost; private CredentialsProvider credentialsProvider; private Lookup authSchemeRegistry; - private AuthCache authCache; private HttpAuthenticator httpAuthenticator; @Before @@ -98,8 +96,6 @@ public class TestHttpAuthenticator { .register(StandardAuthScheme.DIGEST, DigestSchemeFactory.INSTANCE) .register(StandardAuthScheme.NTLM, NTLMSchemeFactory.INSTANCE).build(); this.context.setAttribute(HttpClientContext.AUTHSCHEME_REGISTRY, this.authSchemeRegistry); - this.authCache = Mockito.mock(AuthCache.class); - this.context.setAttribute(HttpClientContext.AUTH_CACHE, this.authCache); this.httpAuthenticator = new HttpAuthenticator(); } @@ -109,7 +105,6 @@ public class TestHttpAuthenticator { response.setHeader(HttpHeaders.WWW_AUTHENTICATE, StandardAuthScheme.BASIC + " realm=test"); Assert.assertTrue(this.httpAuthenticator.isChallenged( this.defaultHost, ChallengeType.TARGET, response, this.authExchange, this.context)); - Mockito.verifyNoInteractions(this.authCache); } @Test @@ -122,8 +117,6 @@ public class TestHttpAuthenticator { Assert.assertTrue(this.httpAuthenticator.isChallenged( this.defaultHost, ChallengeType.TARGET, response, this.authExchange, this.context)); - - Mockito.verify(this.authCache).remove(this.defaultHost); } @Test @@ -144,8 +137,6 @@ public class TestHttpAuthenticator { Assert.assertFalse(this.httpAuthenticator.isChallenged( this.defaultHost, ChallengeType.TARGET, response, this.authExchange, this.context)); Assert.assertEquals(AuthExchange.State.SUCCESS, this.authExchange.getState()); - - Mockito.verify(this.authCache).put(this.defaultHost, this.authScheme); } @Test @@ -157,8 +148,6 @@ public class TestHttpAuthenticator { Assert.assertFalse(this.httpAuthenticator.isChallenged( this.defaultHost, ChallengeType.TARGET, response, this.authExchange, this.context)); Assert.assertEquals(AuthExchange.State.SUCCESS, this.authExchange.getState()); - - Mockito.verify(this.authCache).put(this.defaultHost, this.authScheme); } @Test @@ -269,8 +258,6 @@ public class TestHttpAuthenticator { host, ChallengeType.TARGET, response, authStrategy, this.authExchange, this.context)); Assert.assertEquals(AuthExchange.State.FAILURE, this.authExchange.getState()); - - Mockito.verify(this.authCache).remove(host); } @Test diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestConnectExec.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestConnectExec.java index 730fbd1da..1a743bb11 100644 --- a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestConnectExec.java +++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestConnectExec.java @@ -87,7 +87,7 @@ public class TestConnectExec { @Before public void setup() throws Exception { - exec = new ConnectExec(reuseStrategy, proxyHttpProcessor, proxyAuthStrategy); + exec = new ConnectExec(reuseStrategy, proxyHttpProcessor, proxyAuthStrategy, null, true); target = new HttpHost("foo", 80); proxy = new HttpHost("bar", 8888); } 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 3e687cd41..e0d78dc8a 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 @@ -89,7 +89,7 @@ public class TestProtocolExec { @Before public void setup() throws Exception { - protocolExec = new ProtocolExec(httpProcessor, targetAuthStrategy, proxyAuthStrategy); + protocolExec = new ProtocolExec(httpProcessor, targetAuthStrategy, proxyAuthStrategy, null, true); target = new HttpHost("foo", 80); proxy = new HttpHost("bar", 8888); }